import { zodResolver } from '@hookform/resolvers/zod'; import { useState } from 'react'; import { useForm } from 'react-hook-form'; import * as z from 'zod'; import { useToast } from '@/components/ui/use-toast'; import { apiClient } from '@/lib/api/client'; import type { EffectConfig } from '@/lib/api/types'; import { LANGUAGE_CODES, type LanguageCode } from '@/lib/constants/languages'; import { useGeneration } from '@/lib/hooks/useGeneration'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { useGenerationSettings } from '@/lib/hooks/useSettings'; import { useGenerationStore } from '@/stores/generationStore'; import { useUIStore } from '@/stores/uiStore'; const generationSchema = z.object({ text: z.string().min(1, '').max(50000), language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]), seed: z.number().int().optional(), modelSize: z.enum(['1.7B', '0.6B', '1B', '3B']).optional(), instruct: z.string().max(500).optional(), engine: z .enum([ 'qwen', 'qwen_custom_voice', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada', 'kokoro', ]) .optional(), personality: z.boolean().optional(), }); export type GenerationFormValues = z.infer; interface UseGenerationFormOptions { onSuccess?: (generationId: string) => void; defaultValues?: Partial; getEffectsChain?: () => EffectConfig[] | undefined; } export function useGenerationForm(options: UseGenerationFormOptions = {}) { const { toast } = useToast(); const generation = useGeneration(); const addPendingGeneration = useGenerationStore((state) => state.addPendingGeneration); const { settings: genSettings } = useGenerationSettings(); const maxChunkChars = genSettings?.max_chunk_chars ?? 800; const crossfadeMs = genSettings?.crossfade_ms ?? 50; const normalizeAudio = genSettings?.normalize_audio ?? true; const selectedEngine = useUIStore((state) => state.selectedEngine); const [downloadingModelName, setDownloadingModelName] = useState(null); const [downloadingDisplayName, setDownloadingDisplayName] = useState(null); useModelDownloadToast({ modelName: downloadingModelName || '', displayName: downloadingDisplayName || '', enabled: !!downloadingModelName, }); const form = useForm({ resolver: zodResolver(generationSchema), defaultValues: { text: '', language: 'en', seed: undefined, modelSize: '1.7B', instruct: '', engine: (selectedEngine as GenerationFormValues['engine']) || 'qwen', personality: false, ...options.defaultValues, }, }); async function handleSubmit( data: GenerationFormValues, selectedProfileId: string | null, ): Promise { if (!selectedProfileId) { toast({ title: 'No profile selected', description: 'Please select a voice profile from the cards above.', variant: 'destructive', }); return; } try { const engine = data.engine || 'qwen'; const modelName = engine === 'luxtts' ? 'luxtts' : engine === 'chatterbox' ? 'chatterbox-tts' : engine === 'chatterbox_turbo' ? 'chatterbox-turbo' : engine === 'tada' ? data.modelSize === '3B' ? 'tada-3b-ml' : 'tada-1b' : engine === 'kokoro' ? 'kokoro' : engine === 'qwen_custom_voice' ? `qwen-custom-voice-${data.modelSize}` : `qwen-tts-${data.modelSize}`; const displayName = engine === 'luxtts' ? 'LuxTTS' : engine === 'chatterbox' ? 'Chatterbox TTS' : engine === 'chatterbox_turbo' ? 'Chatterbox Turbo' : engine === 'tada' ? data.modelSize === '3B' ? 'TADA 3B Multilingual' : 'TADA 1B' : engine === 'kokoro' ? 'Kokoro 82M' : engine === 'qwen_custom_voice' ? data.modelSize === '1.7B' ? 'Qwen CustomVoice 1.7B' : 'Qwen CustomVoice 0.6B' : data.modelSize === '1.7B' ? 'Qwen TTS 1.7B' : 'Qwen TTS 0.6B'; // Check if model needs downloading try { const modelStatus = await apiClient.getModelStatus(); const model = modelStatus.models.find((m) => m.model_name === modelName); if (model && !model.downloaded) { setDownloadingModelName(modelName); setDownloadingDisplayName(displayName); } } catch (error) { console.error('Failed to check model status:', error); } const hasModelSizes = engine === 'qwen' || engine === 'qwen_custom_voice' || engine === 'tada'; // Only Qwen CustomVoice actually honors the instruct kwarg at model level. // Base Qwen3-TTS accepts the kwarg but ignores it. const supportsInstruct = engine === 'qwen_custom_voice'; const effectsChain = options.getEffectsChain?.(); // This now returns immediately with status="generating" const result = await generation.mutateAsync({ profile_id: selectedProfileId, text: data.text, language: data.language, seed: data.seed, model_size: hasModelSizes ? data.modelSize : undefined, engine, instruct: supportsInstruct ? data.instruct || undefined : undefined, personality: data.personality || undefined, max_chunk_chars: maxChunkChars, crossfade_ms: crossfadeMs, normalize: normalizeAudio, effects_chain: effectsChain?.length ? effectsChain : undefined, }); // Track this generation for SSE status updates addPendingGeneration(result.id); // Reset form immediately — user can start typing again form.reset({ text: '', language: data.language, seed: undefined, modelSize: data.modelSize, instruct: '', engine: data.engine, personality: data.personality, }); options.onSuccess?.(result.id); } catch (error) { toast({ title: 'Generation failed', description: error instanceof Error ? error.message : 'Failed to generate audio', variant: 'destructive', }); } finally { setDownloadingModelName(null); setDownloadingDisplayName(null); } } return { form, handleSubmit, isPending: generation.isPending, }; }