refactor(*): 代码质量 - 优化前后端实现并完善协议与画布验证
This commit is contained in:
@@ -1,164 +1,50 @@
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { useId } from "react";
|
||||
import { Input, InputNumber, Radio } from "antd";
|
||||
import { Select } from "@/components/ui/base/select";
|
||||
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
||||
import { audioFormatOptionsForConfig, audioSpeedLabel, audioVoiceOptionsForConfig, isDoubaoAudioConfig, normalizeAudioFormatForConfig, normalizeAudioSpeedValue, normalizeAudioVoiceForConfig } from "@/lib/audio-generation";
|
||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
import type { CanvasAudioSettingKey } from "@/components/canvas/canvas-audio-settings-popover";
|
||||
|
||||
const speedOptions = ["0.75", "1", "1.25", "1.5"];
|
||||
type AudioSettingsPanelProps = { config: AiConfig; onConfigChange: (key: CanvasAudioSettingKey, value: string) => void; theme: CanvasTheme; showTitle?: boolean; className?: string };
|
||||
|
||||
type AudioSettingKey =
|
||||
| "audioVoice"
|
||||
| "audioFormat"
|
||||
| "audioSpeed"
|
||||
| "audioLanguage"
|
||||
| "audioDialect"
|
||||
| "audioInstructions"
|
||||
| "audioEmotionControlMethod"
|
||||
| "audioEmotionRandom"
|
||||
| "audioEmotionHappy"
|
||||
| "audioEmotionAngry"
|
||||
| "audioEmotionSad"
|
||||
| "audioEmotionAfraid"
|
||||
| "audioEmotionDisgusted"
|
||||
| "audioEmotionMelancholic"
|
||||
| "audioEmotionSurprised"
|
||||
| "audioEmotionCalm";
|
||||
|
||||
type AudioSettingsPanelProps = {
|
||||
config: AiConfig;
|
||||
onConfigChange: (key: AudioSettingKey, value: string) => void;
|
||||
theme: CanvasTheme;
|
||||
showTitle?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export function AudioSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[var(--panel-width-compact)] space-y-4 rounded-2xl px-1 py-0.5" }: AudioSettingsPanelProps) {
|
||||
const isDoubao = isDoubaoAudioConfig(config);
|
||||
export function AudioSettingsPanel({ config, onConfigChange, theme, showTitle = true, className = "w-[var(--panel-width-compact)] space-y-4" }: AudioSettingsPanelProps) {
|
||||
const listId = useId();
|
||||
const streaming = isDoubaoAudioConfig(config);
|
||||
const voice = normalizeAudioVoiceForConfig(config, config.audioVoice);
|
||||
const voiceOptions = audioVoiceOptionsForConfig(config);
|
||||
const visibleVoiceOptions = voiceOptions.some((item) => item.value === voice) ? voiceOptions : [{ value: voice, label: `当前音色(${voice})` }, ...voiceOptions];
|
||||
const voices = audioVoiceOptionsForConfig(config);
|
||||
const currentVoice = voices.some((item) => item.value === voice) ? voices : [{ value: voice, label: voice || "不指定音色" }, ...voices];
|
||||
const formats = audioFormatOptionsForConfig(config);
|
||||
const format = normalizeAudioFormatForConfig(config, config.audioFormat);
|
||||
const formatOptions = audioFormatOptionsForConfig(config);
|
||||
const speed = normalizeAudioSpeedValue(config.audioSpeed);
|
||||
|
||||
return (
|
||||
<ImageSettingsTheme theme={theme}>
|
||||
<div className={className} style={{ color: theme.node.text }} onMouseDown={(event) => event.stopPropagation()}>
|
||||
{showTitle ? <div className="text-lg font-semibold">音频设置</div> : null}
|
||||
<SettingGroup title="音色" color={theme.node.muted}>
|
||||
{isDoubao ? (
|
||||
<div className="space-y-3">
|
||||
<div className="rounded-xl border px-3 py-2 text-sm leading-6" style={{ borderColor: theme.node.stroke, color: theme.node.muted }}>
|
||||
不连接素材时,按提示词直接生成。连接音频时按参考音频生成,最多 3 段。连接图片时按参考图片生成,最多 1 张。图片和音频不能同时连接。
|
||||
</div>
|
||||
<select
|
||||
value={voice}
|
||||
className="h-9 w-full rounded-xl border bg-transparent px-3 text-sm outline-none"
|
||||
style={{ borderColor: theme.node.stroke, color: theme.node.text, background: theme.spatial.elevated }}
|
||||
onChange={(event) => onConfigChange("audioVoice", event.target.value)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<option value="">不指定音色</option>
|
||||
{voice ? <option value={voice}>当前音色</option> : null}
|
||||
</select>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<input
|
||||
value={config.audioVoice || ""}
|
||||
placeholder="输入渠道音色 ID,例如 voice_001"
|
||||
list="audio-voice-options"
|
||||
className="h-9 w-full rounded-xl border bg-transparent px-3 text-sm outline-none"
|
||||
style={{ borderColor: theme.node.stroke, color: theme.node.text }}
|
||||
onChange={(event) => onConfigChange("audioVoice", event.target.value)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
<datalist id="audio-voice-options">
|
||||
{visibleVoiceOptions.map((item) => (
|
||||
<option key={item.value} value={item.value}>
|
||||
{item.label}
|
||||
</option>
|
||||
))}
|
||||
</datalist>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{visibleVoiceOptions.map((item) => (
|
||||
<OptionPill key={item.value} selected={voice === item.value} theme={theme} onClick={() => onConfigChange("audioVoice", item.value)}>
|
||||
{item.label}
|
||||
</OptionPill>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SettingGroup>
|
||||
<SettingGroup title="格式" color={theme.node.muted}>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{formatOptions.map((item) => (
|
||||
<OptionPill key={item.value} selected={format === item.value} theme={theme} onClick={() => onConfigChange("audioFormat", item.value)}>
|
||||
{item.label}
|
||||
</OptionPill>
|
||||
))}
|
||||
</div>
|
||||
</SettingGroup>
|
||||
<SettingGroup title="语速" color={theme.node.muted}>
|
||||
<div className="grid grid-cols-4 gap-2.5">
|
||||
{speedOptions.map((value) => (
|
||||
<OptionPill key={value} selected={speed === value} theme={theme} onClick={() => onConfigChange("audioSpeed", value)}>
|
||||
{audioSpeedLabel(value)}
|
||||
</OptionPill>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
min={0.25}
|
||||
max={4}
|
||||
step={0.05}
|
||||
className="h-9 w-full rounded-full border bg-transparent px-3 text-center text-sm outline-none [appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none"
|
||||
style={{ borderColor: theme.node.stroke, color: theme.node.text, WebkitTextFillColor: theme.node.text }}
|
||||
value={config.audioSpeed || "1"}
|
||||
onChange={(event) => onConfigChange("audioSpeed", event.target.value)}
|
||||
onBlur={(event) => onConfigChange("audioSpeed", normalizeAudioSpeedValue(event.target.value))}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</SettingGroup>
|
||||
{!isDoubao ? (
|
||||
<SettingGroup title="声音指令" color={theme.node.muted}>
|
||||
<textarea
|
||||
value={config.audioInstructions || ""}
|
||||
placeholder="例如:自然、温暖、适合旁白。"
|
||||
className="thin-scrollbar h-20 w-full resize-none rounded-xl border bg-transparent px-3 py-2 text-sm leading-5 outline-none"
|
||||
style={{ borderColor: theme.node.stroke, color: theme.node.text }}
|
||||
onChange={(event) => onConfigChange("audioInstructions", event.target.value)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</SettingGroup>
|
||||
) : null}
|
||||
</div>
|
||||
</ImageSettingsTheme>
|
||||
);
|
||||
}
|
||||
|
||||
function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="h-9 cursor-pointer rounded-full border px-2 text-sm transition hover:opacity-80"
|
||||
style={{ background: "transparent", borderColor: selected ? theme.node.text : theme.node.stroke, color: theme.node.text }}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
onClick={onClick}
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SettingGroup({ title, color, children }: { title: string; color: string; children: ReactNode }) {
|
||||
return (
|
||||
<div className="space-y-2.5">
|
||||
<div className="text-xs font-medium" style={{ color }}>
|
||||
{title}
|
||||
</div>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
const selectVoice = (value: string) => onConfigChange("audioVoice", value);
|
||||
return <ImageSettingsTheme theme={theme}>
|
||||
<section className={className} style={{ color: theme.node.text }} onPointerDown={(event) => event.stopPropagation()} onMouseDown={(event) => event.stopPropagation()} aria-label="音频生成参数">
|
||||
{showTitle ? <h3 className="text-base font-semibold">音频设置</h3> : null}
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="mb-2 text-xs" style={{ color: theme.node.muted }}>音色</legend>
|
||||
{streaming ? <>
|
||||
<p className="text-xs leading-5" style={{ color: theme.node.muted }}>无参考素材时按提示词生成;可连接最多 3 段音频或 1 张图片,图片与音频不能混用。</p>
|
||||
<Select className="w-full" ariaLabel="音色" value={voice} onChange={selectVoice} options={[{ value: "", label: "不指定音色" }, ...(voice ? [{ value: voice, label: `当前音色 · ${voice}` }] : [])]} />
|
||||
</> : <>
|
||||
<Input aria-label="渠道音色 ID" value={config.audioVoice || ""} list={listId} placeholder="输入渠道音色 ID" onChange={(event) => selectVoice(event.target.value)} />
|
||||
<datalist id={listId}>{currentVoice.map((item) => <option value={item.value} key={item.value}>{item.label}</option>)}</datalist>
|
||||
<Radio.Group aria-label="常用音色" value={voice} onChange={(event) => selectVoice(event.target.value)} options={currentVoice} className="flex flex-wrap gap-2" />
|
||||
</>}
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend className="mb-2 text-xs" style={{ color: theme.node.muted }}>音频格式</legend>
|
||||
<Radio.Group aria-label="音频格式" value={format} options={formats} onChange={(event) => onConfigChange("audioFormat", event.target.value)} className="flex flex-wrap gap-2" />
|
||||
</fieldset>
|
||||
<fieldset className="space-y-2">
|
||||
<legend className="mb-2 text-xs" style={{ color: theme.node.muted }}>语速</legend>
|
||||
<Radio.Group aria-label="常用语速" value={speed} options={["0.75", "1", "1.25", "1.5"].map((value) => ({ value, label: audioSpeedLabel(value) }))} onChange={(event) => onConfigChange("audioSpeed", event.target.value)} />
|
||||
<InputNumber aria-label="自定义语速" min={0.25} max={4} step={0.05} value={Number(speed)} className="w-full" onChange={(value) => { if (value !== null) onConfigChange("audioSpeed", normalizeAudioSpeedValue(String(value))); }} />
|
||||
</fieldset>
|
||||
{!streaming ? <label className="flex flex-col gap-2 text-xs" style={{ color: theme.node.muted }}>声音指令
|
||||
<Input.TextArea aria-label="声音指令" rows={3} value={config.audioInstructions || ""} placeholder="例如:自然、温暖的旁白" onChange={(event) => onConfigChange("audioInstructions", event.target.value)} />
|
||||
</label> : null}
|
||||
</section>
|
||||
</ImageSettingsTheme>;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user