import { useForm, Controller } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import * as z from 'zod' import { useEffect, useState, useMemo } from 'react' import { useTranslation } from 'react-i18next' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Textarea } from '@/components/ui/textarea' import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, DialogFooter } from '@/components/ui/dialog' import { Checkbox } from '@/components/ui/checkbox' import { Label } from '@/components/ui/label' import { Settings, Globe2, Type, Play, FileText, Mic, ArrowRight, ArrowLeft } from 'lucide-react' import { toast } from 'sonner' import { IconLabel } from '@/components/IconLabel' import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip' import { ttsApi, jobApi } from '@/lib/api' import { useJobPolling } from '@/hooks/useJobPolling' import { useHistoryContext } from '@/contexts/HistoryContext' import { LoadingState } from '@/components/LoadingState' import { AudioPlayer } from '@/components/AudioPlayer' import { FileUploader } from '@/components/FileUploader' import { AudioRecorder } from '@/components/AudioRecorder' import { PresetSelector } from '@/components/PresetSelector' import type { Language } from '@/types/tts' import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs' type FormData = { text: string language?: string ref_audio: File ref_text?: string use_cache?: boolean x_vector_only_mode?: boolean max_new_tokens?: number temperature?: number top_k?: number top_p?: number repetition_penalty?: number } function VoiceCloneForm() { const { t } = useTranslation('tts') const { t: tCommon } = useTranslation('common') const { t: tVoice } = useTranslation('voice') const { t: tErrors } = useTranslation('errors') const { t: tConstants } = useTranslation('constants') const PRESET_REF_TEXTS = useMemo(() => tConstants('presetRefTexts', { returnObjects: true }) as Array<{ label: string; text: string }>, [tConstants]) const formSchema = z.object({ text: z.string().min(1, tErrors('validation.required', { field: tErrors('fieldNames.text') })).max(1000, tErrors('validation.maxLength', { field: tErrors('fieldNames.text'), max: 1000 })), language: z.string().optional(), ref_audio: z.instanceof(File, { message: tErrors('validation.required', { field: tErrors('fieldNames.reference_audio') }) }), ref_text: z.string().optional(), use_cache: z.boolean().optional(), x_vector_only_mode: z.boolean().optional(), max_new_tokens: z.number().min(128).max(4096).optional(), temperature: z.number().min(0.1).max(2).optional(), top_k: z.number().min(1).max(100).optional(), top_p: z.number().min(0).max(1).optional(), repetition_penalty: z.number().min(1).max(2).optional(), }) const [languages, setLanguages] = useState([]) const [isLoading, setIsLoading] = useState(false) const [advancedOpen, setAdvancedOpen] = useState(false) const [step, setStep] = useState<1 | 2>(1) const [inputTab, setInputTab] = useState<'upload' | 'record'>('upload') const [tempAdvancedParams, setTempAdvancedParams] = useState({ max_new_tokens: 2048 }) const { currentJob, isPolling, isCompleted, startPolling, elapsedTime } = useJobPolling() const { refresh } = useHistoryContext() const { register, handleSubmit, setValue, watch, control, trigger, formState: { errors }, } = useForm({ resolver: zodResolver(formSchema), defaultValues: { text: '', language: 'Auto', ref_text: '', use_cache: true, x_vector_only_mode: false, max_new_tokens: 2048, temperature: 0.9, top_k: 50, top_p: 1.0, repetition_penalty: 1.05, } as Partial, }) useEffect(() => { const fetchData = async () => { try { const langs = await ttsApi.getLanguages() setLanguages(langs) } catch (error) { toast.error(t('loadDataFailed')) } } fetchData() }, [t]) useEffect(() => { if (inputTab === 'record' && PRESET_REF_TEXTS.length > 0) { setValue('ref_text', PRESET_REF_TEXTS[0].text) } else if (inputTab === 'upload') { setValue('ref_text', '') } }, [inputTab, setValue]) const handleNextStep = async () => { // Validate step 1 fields const valid = await trigger(['ref_audio', 'ref_text']) if (valid) { setStep(2) } } const onSubmit = async (data: FormData) => { setIsLoading(true) try { const result = await ttsApi.createVoiceCloneJob({ ...data, ref_audio: data.ref_audio, }) toast.success(t('taskCreated')) startPolling(result.job_id) try { await refresh() } catch { } } catch (error) { toast.error(t('taskCreateFailed')) } finally { setIsLoading(false) } } const memoizedAudioUrl = useMemo(() => { if (!currentJob) return '' return jobApi.getAudioUrl(currentJob.id, currentJob.audio_url) }, [currentJob?.id, currentJob?.audio_url]) return (
{/* Steps Indicator */}
1
{tVoice('step1Title')}
2
{tVoice('step2Title')}
{/* Step 1: Input Selection */} setInputTab(v as any)} className="w-full"> {tVoice('uploadTab')} {tVoice('recordTab')}
( )} />