mirror of
https://github.com/basketikun/infinite-canvas.git
synced 2026-08-05 00:34:22 +08:00
feat(audio): add audio generation capabilities with configurable settings and UI enhancements
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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)} />
|
||||
|
||||
Reference in New Issue
Block a user