fix(frontend): #1898 配音库AI配音增加情绪语言选择 (#1904)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / Integration Tests (push) Successful in 4m1s
CI/CD Pipeline / Validate - Style (push) Successful in 4m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m12s
CI/CD Pipeline / Unit Tests (push) Successful in 10m22s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 45h15m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 45h17m31s
CI/CD Pipeline / PR Build API Image (push) Failing after 45h17m8s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 45h18m15s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 45h15m18s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 45h15m19s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 45h16m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 45h17m41s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check push changed paths (push) Successful in 12s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 16s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 27s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m30s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m6s
CI/CD Pipeline / Integration Tests (push) Successful in 4m1s
CI/CD Pipeline / Validate - Style (push) Successful in 4m56s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m3s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m43s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m12s
CI/CD Pipeline / Unit Tests (push) Successful in 10m22s
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Failing after 45h15m46s
CI/CD Pipeline / PR Build Web Image (push) Failing after 45h17m31s
CI/CD Pipeline / PR Build API Image (push) Failing after 45h17m8s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 45h18m15s
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Failing after 45h15m18s
CI/CD Pipeline / Retag skipped Staging API Image (push) Failing after 45h15m19s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 45h16m58s
CI/CD Pipeline / Frontend Lint (push) Failing after 45h17m41s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1904.
This commit is contained in:
@@ -16,6 +16,7 @@ export interface TTSSynthesizeRequest {
|
||||
voice_id?: string
|
||||
output_name?: string
|
||||
language?: string
|
||||
emotion?: string
|
||||
speed?: number
|
||||
voice_model?: string
|
||||
voice_clone_profile_id?: string
|
||||
@@ -103,6 +104,7 @@ export interface TTSPreviewRequest {
|
||||
voice_id: string
|
||||
speed?: number
|
||||
pitch?: number
|
||||
language?: string
|
||||
emotion?: string // 情绪参数:natural/excited/calm/friendly
|
||||
}
|
||||
|
||||
|
||||
@@ -132,12 +132,16 @@ const VoiceLibrary: React.FC = () => {
|
||||
ttsText,
|
||||
ttsVoiceId,
|
||||
ttsSpeed,
|
||||
ttsEmotion,
|
||||
ttsLanguage,
|
||||
ttsStatus,
|
||||
ttsAudioUrl,
|
||||
ttsError,
|
||||
setTtsText,
|
||||
setTtsVoiceId,
|
||||
setTtsSpeed,
|
||||
setTtsEmotion,
|
||||
setTtsLanguage,
|
||||
setTtsOpen,
|
||||
handleTtsSynthesize,
|
||||
handleTtsSave,
|
||||
@@ -363,6 +367,8 @@ const VoiceLibrary: React.FC = () => {
|
||||
ttsText={ttsText}
|
||||
ttsVoiceId={ttsVoiceId}
|
||||
ttsSpeed={ttsSpeed}
|
||||
ttsEmotion={ttsEmotion}
|
||||
ttsLanguage={ttsLanguage}
|
||||
ttsStatus={ttsStatus}
|
||||
ttsAudioUrl={ttsAudioUrl}
|
||||
ttsError={ttsError}
|
||||
@@ -374,6 +380,8 @@ const VoiceLibrary: React.FC = () => {
|
||||
onTtsTextChange={setTtsText}
|
||||
onTtsVoiceChange={setTtsVoiceId}
|
||||
onTtsSpeedChange={setTtsSpeed}
|
||||
onTtsEmotionChange={setTtsEmotion}
|
||||
onTtsLanguageChange={setTtsLanguage}
|
||||
onTtsSynthesize={handleTtsSynthesize}
|
||||
onTtsSave={handleTtsSave}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,8 @@ import { type TtsModalProps, type TtsStatus } from "./tts-modal/types"
|
||||
import TextInputSection from "./tts-modal/TextInputSection"
|
||||
import VoiceSelector from "./tts-modal/VoiceSelector"
|
||||
import SpeedControl from "./tts-modal/SpeedControl"
|
||||
import EmotionControl from "./tts-modal/EmotionControl"
|
||||
import LanguageControl from "./tts-modal/LanguageControl"
|
||||
import SynthesizeButton from "./tts-modal/SynthesizeButton"
|
||||
import ErrorAlert from "./tts-modal/ErrorAlert"
|
||||
import ResultPanel from "./tts-modal/ResultPanel"
|
||||
@@ -14,6 +16,8 @@ const TtsModal: React.FC<TtsModalProps> = ({
|
||||
ttsText,
|
||||
ttsVoiceId,
|
||||
ttsSpeed,
|
||||
ttsEmotion,
|
||||
ttsLanguage,
|
||||
ttsStatus,
|
||||
ttsAudioUrl,
|
||||
ttsError,
|
||||
@@ -23,6 +27,8 @@ const TtsModal: React.FC<TtsModalProps> = ({
|
||||
onTextChange,
|
||||
onVoiceChange,
|
||||
onSpeedChange,
|
||||
onEmotionChange,
|
||||
onLanguageChange,
|
||||
onSynthesize,
|
||||
onSave,
|
||||
}) => {
|
||||
@@ -43,6 +49,16 @@ const TtsModal: React.FC<TtsModalProps> = ({
|
||||
presetVoices={presetVoices}
|
||||
clonedVoices={clonedVoices}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "1fr 1fr",
|
||||
gap: 12,
|
||||
}}
|
||||
>
|
||||
<EmotionControl emotion={ttsEmotion} onChange={onEmotionChange} />
|
||||
<LanguageControl language={ttsLanguage} onChange={onLanguageChange} />
|
||||
</div>
|
||||
<SpeedControl speed={ttsSpeed} onChange={onSpeedChange} />
|
||||
<SynthesizeButton status={ttsStatus} text={ttsText} onClick={onSynthesize} />
|
||||
{ttsError && <ErrorAlert error={ttsError} />}
|
||||
|
||||
@@ -6,6 +6,7 @@ import type { ClonedVoiceDisplay, PresetVoiceDisplay } from "../types"
|
||||
import type { VoiceClone } from "@/api/voice-clone"
|
||||
import type { TtsStatus } from "./TtsModal"
|
||||
import type { TtsClonedVoiceOption } from "./tts-modal/VoiceSelector"
|
||||
import type { TtsEmotion, TtsLanguage } from "./tts-modal/constants"
|
||||
import CloneModal from "@/components/voice/CloneModal"
|
||||
import CloneDetailModal from "./CloneDetailModal"
|
||||
import UploadVoiceModal from "./UploadVoiceModal"
|
||||
@@ -42,6 +43,8 @@ export interface VoiceModalsProps {
|
||||
ttsText: string
|
||||
ttsVoiceId: string
|
||||
ttsSpeed: number
|
||||
ttsEmotion: TtsEmotion
|
||||
ttsLanguage: TtsLanguage
|
||||
ttsStatus: TtsStatus
|
||||
ttsAudioUrl: string | null
|
||||
ttsError: string | null
|
||||
@@ -52,6 +55,8 @@ export interface VoiceModalsProps {
|
||||
onTtsTextChange: (text: string) => void
|
||||
onTtsVoiceChange: (id: string) => void
|
||||
onTtsSpeedChange: (speed: number) => void
|
||||
onTtsEmotionChange: (emotion: TtsEmotion) => void
|
||||
onTtsLanguageChange: (language: TtsLanguage) => void
|
||||
onTtsSynthesize: () => void
|
||||
onTtsSave: () => void
|
||||
}
|
||||
@@ -80,6 +85,8 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
ttsText,
|
||||
ttsVoiceId,
|
||||
ttsSpeed,
|
||||
ttsEmotion,
|
||||
ttsLanguage,
|
||||
ttsStatus,
|
||||
ttsAudioUrl,
|
||||
ttsError,
|
||||
@@ -89,6 +96,8 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
onTtsTextChange,
|
||||
onTtsVoiceChange,
|
||||
onTtsSpeedChange,
|
||||
onTtsEmotionChange,
|
||||
onTtsLanguageChange,
|
||||
onTtsSynthesize,
|
||||
onTtsSave,
|
||||
}) => {
|
||||
@@ -129,6 +138,8 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
ttsText={ttsText}
|
||||
ttsVoiceId={ttsVoiceId}
|
||||
ttsSpeed={ttsSpeed}
|
||||
ttsEmotion={ttsEmotion}
|
||||
ttsLanguage={ttsLanguage}
|
||||
ttsStatus={ttsStatus}
|
||||
ttsAudioUrl={ttsAudioUrl}
|
||||
ttsError={ttsError}
|
||||
@@ -138,6 +149,8 @@ export const VoiceModals: React.FC<VoiceModalsProps> = ({
|
||||
onTextChange={onTtsTextChange}
|
||||
onVoiceChange={onTtsVoiceChange}
|
||||
onSpeedChange={onTtsSpeedChange}
|
||||
onEmotionChange={onTtsEmotionChange}
|
||||
onLanguageChange={onTtsLanguageChange}
|
||||
onSynthesize={onTtsSynthesize}
|
||||
onSave={onTtsSave}
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react"
|
||||
import { TTS_EMOTION_OPTIONS, type TtsEmotion } from "./constants"
|
||||
|
||||
interface EmotionControlProps {
|
||||
emotion: TtsEmotion
|
||||
onChange: (emotion: TtsEmotion) => void
|
||||
}
|
||||
|
||||
/** 情绪选择下拉 */
|
||||
const EmotionControl: React.FC<EmotionControlProps> = ({ emotion, onChange }) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--text-secondary)",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
情绪
|
||||
</div>
|
||||
<select
|
||||
value={emotion}
|
||||
onChange={(e) => onChange(e.target.value as TtsEmotion)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid var(--border-color, #e5e7eb)",
|
||||
background: "var(--bg-primary, #fff)",
|
||||
color: "var(--text-primary)",
|
||||
fontSize: 13,
|
||||
outline: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{TTS_EMOTION_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default EmotionControl
|
||||
@@ -0,0 +1,47 @@
|
||||
import React from "react"
|
||||
import { TTS_LANGUAGE_OPTIONS, type TtsLanguage } from "./constants"
|
||||
|
||||
interface LanguageControlProps {
|
||||
language: TtsLanguage
|
||||
onChange: (language: TtsLanguage) => void
|
||||
}
|
||||
|
||||
/** 语言选择下拉 */
|
||||
const LanguageControl: React.FC<LanguageControlProps> = ({ language, onChange }) => {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: 13,
|
||||
color: "var(--text-secondary)",
|
||||
marginBottom: 6,
|
||||
}}
|
||||
>
|
||||
语言
|
||||
</div>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => onChange(e.target.value as TtsLanguage)}
|
||||
style={{
|
||||
width: "100%",
|
||||
padding: "7px 10px",
|
||||
borderRadius: 6,
|
||||
border: "1px solid var(--border-color, #e5e7eb)",
|
||||
background: "var(--bg-primary, #fff)",
|
||||
color: "var(--text-primary)",
|
||||
fontSize: 13,
|
||||
outline: "none",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
{TTS_LANGUAGE_OPTIONS.map((opt) => (
|
||||
<option key={opt.value} value={opt.value}>
|
||||
{opt.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LanguageControl
|
||||
@@ -0,0 +1,22 @@
|
||||
/** TTS 情绪选项(对齐后端 CosyVoice 支持:natural/excited/calm/friendly) */
|
||||
export const TTS_EMOTION_OPTIONS = [
|
||||
{ value: "natural", label: "自然" },
|
||||
{ value: "excited", label: "兴奋" },
|
||||
{ value: "calm", label: "沉稳" },
|
||||
{ value: "friendly", label: "亲切" },
|
||||
] as const
|
||||
|
||||
export type TtsEmotion = (typeof TTS_EMOTION_OPTIONS)[number]["value"]
|
||||
|
||||
/** TTS 语言选项 */
|
||||
export const TTS_LANGUAGE_OPTIONS = [
|
||||
{ value: "zh-CN", label: "中文" },
|
||||
{ value: "en", label: "英文" },
|
||||
{ value: "ja", label: "日文" },
|
||||
{ value: "ko", label: "韩文" },
|
||||
] as const
|
||||
|
||||
export type TtsLanguage = (typeof TTS_LANGUAGE_OPTIONS)[number]["value"]
|
||||
|
||||
export const DEFAULT_TTS_EMOTION: TtsEmotion = "natural"
|
||||
export const DEFAULT_TTS_LANGUAGE: TtsLanguage = "zh-CN"
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type PresetVoiceDisplay } from "@/pages/voices/types"
|
||||
import type { TtsClonedVoiceOption } from "./VoiceSelector"
|
||||
import type { TtsEmotion, TtsLanguage } from "./constants"
|
||||
|
||||
export type TtsStatus = "idle" | "synthesizing" | "done" | "error"
|
||||
|
||||
@@ -8,6 +9,8 @@ export interface TtsModalProps {
|
||||
ttsText: string
|
||||
ttsVoiceId: string
|
||||
ttsSpeed: number
|
||||
ttsEmotion: TtsEmotion
|
||||
ttsLanguage: TtsLanguage
|
||||
ttsStatus: TtsStatus
|
||||
ttsAudioUrl: string | null
|
||||
ttsError: string | null
|
||||
@@ -18,6 +21,8 @@ export interface TtsModalProps {
|
||||
onTextChange: (text: string) => void
|
||||
onVoiceChange: (voiceId: string) => void
|
||||
onSpeedChange: (speed: number) => void
|
||||
onEmotionChange: (emotion: TtsEmotion) => void
|
||||
onLanguageChange: (language: TtsLanguage) => void
|
||||
onSynthesize: () => void
|
||||
onSave: () => void
|
||||
}
|
||||
|
||||
@@ -4,6 +4,12 @@ import { message } from "antd"
|
||||
import { synthesizeSpeech, getTTSJobStatus, saveTtsToLibrary } from "@/api/tts"
|
||||
import { type PresetVoiceDisplay } from "../types"
|
||||
import type { TtsClonedVoiceOption } from "../components/tts-modal/VoiceSelector"
|
||||
import {
|
||||
DEFAULT_TTS_EMOTION,
|
||||
DEFAULT_TTS_LANGUAGE,
|
||||
type TtsEmotion,
|
||||
type TtsLanguage,
|
||||
} from "../components/tts-modal/constants"
|
||||
|
||||
export type TtsStatus = "idle" | "synthesizing" | "done" | "error"
|
||||
|
||||
@@ -29,6 +35,8 @@ export function useTtsSynthesize({
|
||||
const [ttsText, setTtsText] = useState("")
|
||||
const [ttsVoiceId, setTtsVoiceId] = useState<string>("")
|
||||
const [ttsSpeed, setTtsSpeed] = useState(1.0)
|
||||
const [ttsEmotion, setTtsEmotion] = useState<TtsEmotion>(DEFAULT_TTS_EMOTION)
|
||||
const [ttsLanguage, setTtsLanguage] = useState<TtsLanguage>(DEFAULT_TTS_LANGUAGE)
|
||||
const [ttsJobId, setTtsJobId] = useState<string | null>(null)
|
||||
const [ttsStatus, setTtsStatus] = useState<TtsStatus>("idle")
|
||||
const [ttsAudioUrl, setTtsAudioUrl] = useState<string | null>(null)
|
||||
@@ -51,6 +59,8 @@ export function useTtsSynthesize({
|
||||
text: ttsText.trim(),
|
||||
voice_id: ttsVoiceId || undefined,
|
||||
speed: ttsSpeed,
|
||||
emotion: ttsEmotion,
|
||||
language: ttsLanguage,
|
||||
})
|
||||
setTtsJobId(resp.job_id)
|
||||
|
||||
@@ -81,7 +91,7 @@ export function useTtsSynthesize({
|
||||
setTtsStatus("error")
|
||||
setTtsError(msg)
|
||||
}
|
||||
}, [ttsText, ttsVoiceId, ttsSpeed])
|
||||
}, [ttsText, ttsVoiceId, ttsSpeed, ttsEmotion, ttsLanguage])
|
||||
|
||||
/** 保存 TTS 结果到素材库 */
|
||||
const handleTtsSave = useCallback(async () => {
|
||||
@@ -103,6 +113,8 @@ export function useTtsSynthesize({
|
||||
setTtsText("")
|
||||
setTtsVoiceId("")
|
||||
setTtsSpeed(1.0)
|
||||
setTtsEmotion(DEFAULT_TTS_EMOTION)
|
||||
setTtsLanguage(DEFAULT_TTS_LANGUAGE)
|
||||
setTtsStatus("idle")
|
||||
setTtsAudioUrl(null)
|
||||
setTtsError(null)
|
||||
@@ -132,6 +144,8 @@ export function useTtsSynthesize({
|
||||
ttsText,
|
||||
ttsVoiceId,
|
||||
ttsSpeed,
|
||||
ttsEmotion,
|
||||
ttsLanguage,
|
||||
ttsJobId,
|
||||
ttsStatus,
|
||||
ttsAudioUrl,
|
||||
@@ -143,6 +157,8 @@ export function useTtsSynthesize({
|
||||
setTtsText,
|
||||
setTtsVoiceId,
|
||||
setTtsSpeed,
|
||||
setTtsEmotion,
|
||||
setTtsLanguage,
|
||||
setTtsOpen,
|
||||
// Actions
|
||||
handleTtsSynthesize,
|
||||
|
||||
Reference in New Issue
Block a user