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 { LANGUAGE_CODES, type LanguageCode } from '@/lib/constants/languages'; import { useGeneration } from '@/lib/hooks/useGeneration'; import { useModelDownloadToast } from '@/lib/hooks/useModelDownloadToast'; import { useGenerationStore } from '@/stores/generationStore'; import { useServerStore } from '@/stores/serverStore'; const generationSchema = z.object({ text: z.string().min(1, 'Text is required').max(50000), language: z.enum(LANGUAGE_CODES as [LanguageCode, ...LanguageCode[]]), seed: z.number().int().optional(), modelSize: z.enum(['1.7B', '0.6B']).optional(), instruct: z.string().max(500).optional(), engine: z.enum(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo']).optional(), }); export type GenerationFormValues = z.infer; interface UseGenerationFormOptions { onSuccess?: (generationId: string) => void; defaultValues?: Partial; } export function useGenerationForm(options: UseGenerationFormOptions = {}) { const { toast } = useToast(); const generation = useGeneration(); const addPendingGeneration = useGenerationStore((state) => state.addPendingGeneration); const maxChunkChars = useServerStore((state) => state.maxChunkChars); const crossfadeMs = useServerStore((state) => state.crossfadeMs); const normalizeAudio = useServerStore((state) => state.normalizeAudio); 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: 'qwen', ...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' : `qwen-tts-${data.modelSize}`; const displayName = engine === 'luxtts' ? 'LuxTTS' : engine === 'chatterbox' ? 'Chatterbox TTS' : engine === 'chatterbox_turbo' ? 'Chatterbox Turbo' : 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 isQwen = engine === 'qwen'; // 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: isQwen ? data.modelSize : undefined, engine, instruct: isQwen ? data.instruct || undefined : undefined, max_chunk_chars: maxChunkChars, crossfade_ms: crossfadeMs, normalize: normalizeAudio, }); // 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, }); 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, }; }