feat(web): 角色与音频工作台 - 完善素材选择、生成参数与同步交互
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { type ReactNode } from "react";
|
||||
|
||||
import { ImageSettingsTheme } from "@/components/image-settings-panel";
|
||||
import { audioFormatOptions, audioSpeedLabel, audioVoiceOptions, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
|
||||
import { audioFormatOptionsForConfig, audioSpeedLabel, audioVoiceOptionsForConfig, isDoubaoAudioConfig, normalizeAudioFormatForConfig, normalizeAudioSpeedValue, normalizeAudioVoiceForConfig } from "@/lib/audio-generation";
|
||||
import { type CanvasTheme } from "@/lib/canvas-theme";
|
||||
import type { AiConfig } from "@/stores/use-config-store";
|
||||
|
||||
@@ -11,6 +11,8 @@ type AudioSettingKey =
|
||||
| "audioVoice"
|
||||
| "audioFormat"
|
||||
| "audioSpeed"
|
||||
| "audioLanguage"
|
||||
| "audioDialect"
|
||||
| "audioInstructions"
|
||||
| "audioEmotionControlMethod"
|
||||
| "audioEmotionRandom"
|
||||
@@ -32,26 +34,62 @@ type AudioSettingsPanelProps = {
|
||||
};
|
||||
|
||||
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 voice = normalizeAudioVoiceValue(config.audioVoice);
|
||||
const format = normalizeAudioFormatValue(config.audioFormat);
|
||||
const isDoubao = 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 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}>
|
||||
<div className="grid grid-cols-3 gap-2.5">
|
||||
{audioVoiceOptions.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}>
|
||||
{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">
|
||||
{audioFormatOptions.map((item) => (
|
||||
{formatOptions.map((item) => (
|
||||
<OptionPill key={item.value} selected={format === item.value} theme={theme} onClick={() => onConfigChange("audioFormat", item.value)}>
|
||||
{item.label}
|
||||
</OptionPill>
|
||||
@@ -79,7 +117,7 @@ export function AudioSettingsPanel({ config, onConfigChange, theme, showTitle =
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</SettingGroup>
|
||||
<SettingGroup title="声音指令" color={theme.node.muted}>
|
||||
{!isDoubao ? <SettingGroup title="声音指令" color={theme.node.muted}>
|
||||
<textarea
|
||||
value={config.audioInstructions || ""}
|
||||
placeholder="例如:自然、温暖、适合旁白。"
|
||||
@@ -88,64 +126,12 @@ export function AudioSettingsPanel({ config, onConfigChange, theme, showTitle =
|
||||
onChange={(event) => onConfigChange("audioInstructions", event.target.value)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</SettingGroup>
|
||||
<SettingGroup title="情感控制(IndexTTS2)" color={theme.node.muted}>
|
||||
<select
|
||||
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 }}
|
||||
value={config.audioEmotionControlMethod || "与音色参考音频相同"}
|
||||
onChange={(event) => onConfigChange("audioEmotionControlMethod", event.target.value)}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
>
|
||||
<option value="与音色参考音频相同">与音色参考音频相同</option>
|
||||
<option value="使用情感参考音频">使用情感参考音频</option>
|
||||
</select>
|
||||
<label className="flex items-center justify-between gap-3 text-sm">
|
||||
<span>随机情感</span>
|
||||
<input type="checkbox" checked={config.audioEmotionRandom === "true"} onChange={(event) => onConfigChange("audioEmotionRandom", String(event.target.checked))} />
|
||||
</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{emotionFields.map(([key, label]) => (
|
||||
<label key={key} className="flex items-center gap-2 text-xs">
|
||||
<span className="min-w-0 flex-1">{label}</span>
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
className="h-8 w-20 rounded-lg border bg-transparent px-2 text-right outline-none"
|
||||
style={{ borderColor: theme.node.stroke, color: theme.node.text }}
|
||||
value={config[key] || "0"}
|
||||
onChange={(event) => onConfigChange(key, event.target.value)}
|
||||
onBlur={(event) => onConfigChange(key, normalizeEmotionWeight(event.target.value))}
|
||||
onMouseDown={(event) => event.stopPropagation()}
|
||||
/>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</SettingGroup>
|
||||
</SettingGroup> : null}
|
||||
</div>
|
||||
</ImageSettingsTheme>
|
||||
);
|
||||
}
|
||||
|
||||
const emotionFields: Array<[Extract<AudioSettingKey, `audioEmotion${string}`>, string]> = [
|
||||
["audioEmotionHappy", "快乐"],
|
||||
["audioEmotionAngry", "愤怒"],
|
||||
["audioEmotionSad", "悲伤"],
|
||||
["audioEmotionAfraid", "害怕"],
|
||||
["audioEmotionDisgusted", "厌恶"],
|
||||
["audioEmotionMelancholic", "忧郁"],
|
||||
["audioEmotionSurprised", "惊讶"],
|
||||
["audioEmotionCalm", "平静"],
|
||||
];
|
||||
|
||||
function normalizeEmotionWeight(value: string) {
|
||||
const number = Number(value);
|
||||
if (!Number.isFinite(number)) return "0";
|
||||
return String(Math.max(0, Math.min(1, number)));
|
||||
}
|
||||
|
||||
function OptionPill({ selected, theme, onClick, children }: { selected: boolean; theme: CanvasTheme; onClick: () => void; children: ReactNode }) {
|
||||
return (
|
||||
<button
|
||||
|
||||
Reference in New Issue
Block a user