feat(audio): add audio generation capabilities with configurable settings and UI enhancements

This commit is contained in:
HouYunFei
2026-06-02 17:50:36 +08:00
parent 38ec654748
commit 8f19a18c35
14 changed files with 528 additions and 18 deletions
+102
View File
@@ -0,0 +1,102 @@
"use client";
import { type ReactNode } from "react";
import { ImageSettingsTheme } from "@/components/image-settings-panel";
import { audioFormatOptions, audioSpeedLabel, audioVoiceOptions, normalizeAudioFormatValue, normalizeAudioSpeedValue, normalizeAudioVoiceValue } from "@/lib/audio-generation";
import { type CanvasTheme } from "@/lib/canvas-theme";
import type { AiConfig } from "@/stores/use-config-store";
const speedOptions = ["0.75", "1", "1.25", "1.5"];
type AudioSettingKey = "audioVoice" | "audioFormat" | "audioSpeed" | "audioInstructions";
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-[320px] space-y-4 rounded-2xl px-1 py-0.5" }: AudioSettingsPanelProps) {
const voice = normalizeAudioVoiceValue(config.audioVoice);
const format = normalizeAudioFormatValue(config.audioFormat);
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}>
<div className="grid grid-cols-3 gap-2.5">
{audioFormatOptions.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>
<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>
</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>
);
}
+22 -1
View File
@@ -5,6 +5,7 @@ import { useState } from "react";
import { ModelPicker } from "@/components/model-picker";
import { fetchImageModels } from "@/services/api/image";
import { audioFormatOptions, audioVoiceOptions, normalizeAudioSpeedValue } from "@/lib/audio-generation";
import { filterModelsByCapability, useConfigStore, useEffectiveConfig, type AiConfig, type ModelCapability } from "@/stores/use-config-store";
type ModelGroup = {
@@ -180,7 +181,7 @@ export function AppConfigModal() {
</Form.Item>
))}
</div>
<div className="grid gap-4 md:grid-cols-3">
<div className="grid gap-4 md:grid-cols-4">
<Form.Item label="画布默认生图张数" extra="新建画布生图和配置节点默认使用,单个节点仍可单独覆盖。" className="mb-4">
<Input
type="number"
@@ -191,7 +192,27 @@ export function AppConfigModal() {
onBlur={(event) => updateConfig("canvasImageCount", normalizeImageCount(event.target.value))}
/>
</Form.Item>
<Form.Item label="默认音频声音" className="mb-4">
<Select value={config.audioVoice} options={audioVoiceOptions} onChange={(value) => updateConfig("audioVoice", value)} />
</Form.Item>
<Form.Item label="默认音频格式" className="mb-4">
<Select value={config.audioFormat} options={audioFormatOptions} onChange={(value) => updateConfig("audioFormat", value)} />
</Form.Item>
<Form.Item label="默认音频语速" className="mb-4">
<Input
type="number"
min={0.25}
max={4}
step={0.05}
value={config.audioSpeed}
onChange={(event) => updateConfig("audioSpeed", event.target.value)}
onBlur={(event) => updateConfig("audioSpeed", normalizeAudioSpeedValue(event.target.value))}
/>
</Form.Item>
</div>
<Form.Item label="默认音频指令" className="mb-4">
<Input.TextArea rows={2} value={config.audioInstructions} placeholder="例如:自然、温暖、适合旁白。" onChange={(event) => updateConfig("audioInstructions", event.target.value)} />
</Form.Item>
{effectiveMode === "local" ? (
<Form.Item label="系统提示词" className="mb-0">
<Input.TextArea rows={3} value={config.systemPrompt} placeholder="例如:你是一位擅长电影感写实摄影的视觉导演。" onChange={(event) => updateConfig("systemPrompt", event.target.value)} />