import { useEffect } from 'react'; import type { UseFormReturn } from 'react-hook-form'; import { FormControl } from '@/components/ui/form'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import type { VoiceProfileResponse } from '@/lib/api/types'; import { getLanguageOptionsForEngine } from '@/lib/constants/languages'; import type { GenerationFormValues } from '@/lib/hooks/useGenerationForm'; /** * Engine/model options and their display metadata. * Adding a new engine means adding one entry here. */ const ENGINE_OPTIONS = [ { value: 'qwen:1.7B', label: 'Qwen3-TTS 1.7B', engine: 'qwen' }, { value: 'qwen:0.6B', label: 'Qwen3-TTS 0.6B', engine: 'qwen' }, { value: 'qwen_custom_voice:1.7B', label: 'Qwen CustomVoice 1.7B', engine: 'qwen_custom_voice' }, { value: 'qwen_custom_voice:0.6B', label: 'Qwen CustomVoice 0.6B', engine: 'qwen_custom_voice' }, { value: 'luxtts', label: 'LuxTTS', engine: 'luxtts' }, { value: 'chatterbox', label: 'Chatterbox', engine: 'chatterbox' }, { value: 'chatterbox_turbo', label: 'Chatterbox Turbo', engine: 'chatterbox_turbo' }, { value: 'tada:1B', label: 'TADA 1B', engine: 'tada' }, { value: 'tada:3B', label: 'TADA 3B Multilingual', engine: 'tada' }, { value: 'kokoro', label: 'Kokoro 82M', engine: 'kokoro' }, ] as const; const ENGINE_DESCRIPTIONS: Record = { qwen: 'Multi-language, two sizes', qwen_custom_voice: '9 preset voices, instruct control', luxtts: 'Fast, English-focused', chatterbox: '23 languages, incl. Hebrew', chatterbox_turbo: 'English, [laugh] [cough] tags', tada: 'HumeAI, 700s+ coherent audio', kokoro: '82M params, CPU realtime, 8 langs', }; /** Engines that only support English and should force language to 'en' on select. */ const ENGLISH_ONLY_ENGINES = new Set(['luxtts', 'chatterbox_turbo']); /** Engines that support cloned (reference audio) profiles. */ const CLONING_ENGINES = new Set(['qwen', 'luxtts', 'chatterbox', 'chatterbox_turbo', 'tada']); function getAvailableOptions(selectedProfile?: VoiceProfileResponse | null) { if (!selectedProfile) return ENGINE_OPTIONS; return ENGINE_OPTIONS.filter((opt) => isProfileCompatibleWithEngine(selectedProfile, opt.engine)); } function getSelectValue(engine: string, modelSize?: string): string { if (engine === 'qwen') return `qwen:${modelSize || '1.7B'}`; if (engine === 'qwen_custom_voice') return `qwen_custom_voice:${modelSize || '1.7B'}`; if (engine === 'tada') return `tada:${modelSize || '1B'}`; return engine; } export function applyEngineSelection(form: UseFormReturn, value: string) { if (value.startsWith('qwen_custom_voice:')) { const [, modelSize] = value.split(':'); form.setValue('engine', 'qwen_custom_voice'); form.setValue('modelSize', modelSize as '1.7B' | '0.6B'); const currentLang = form.getValues('language'); const available = getLanguageOptionsForEngine('qwen_custom_voice'); if (!available.some((l) => l.value === currentLang)) { form.setValue('language', available[0]?.value ?? 'en'); } } else if (value.startsWith('qwen:')) { const [, modelSize] = value.split(':'); form.setValue('engine', 'qwen'); form.setValue('modelSize', modelSize as '1.7B' | '0.6B'); // Validate language is supported by Qwen const currentLang = form.getValues('language'); const available = getLanguageOptionsForEngine('qwen'); if (!available.some((l) => l.value === currentLang)) { form.setValue('language', available[0]?.value ?? 'en'); } } else if (value.startsWith('tada:')) { const [, modelSize] = value.split(':'); form.setValue('engine', 'tada'); form.setValue('modelSize', modelSize as '1B' | '3B'); // TADA 1B is English-only; 3B is multilingual if (modelSize === '1B') { form.setValue('language', 'en'); } else { const currentLang = form.getValues('language'); const available = getLanguageOptionsForEngine('tada'); if (!available.some((l) => l.value === currentLang)) { form.setValue('language', available[0]?.value ?? 'en'); } } } else { form.setValue('engine', value as GenerationFormValues['engine']); form.setValue('modelSize', undefined as unknown as '1.7B' | '0.6B'); if (ENGLISH_ONLY_ENGINES.has(value)) { form.setValue('language', 'en'); } else { // If current language isn't supported by the new engine, reset to first available const currentLang = form.getValues('language'); const available = getLanguageOptionsForEngine(value); if (!available.some((l) => l.value === currentLang)) { form.setValue('language', available[0]?.value ?? 'en'); } } } } interface EngineModelSelectorProps { form: UseFormReturn; compact?: boolean; selectedProfile?: VoiceProfileResponse | null; } export function EngineModelSelector({ form, compact, selectedProfile }: EngineModelSelectorProps) { const engine = form.watch('engine') || 'qwen'; const modelSize = form.watch('modelSize'); const selectValue = getSelectValue(engine, modelSize); const availableOptions = getAvailableOptions(selectedProfile); const currentEngineAvailable = availableOptions.some((opt) => opt.value === selectValue); useEffect(() => { if (!currentEngineAvailable && availableOptions.length > 0) { applyEngineSelection(form, availableOptions[0].value); } }, [availableOptions, currentEngineAvailable, form]); const itemClass = compact ? 'text-xs text-muted-foreground' : undefined; const triggerClass = compact ? 'h-8 text-xs bg-card border-border rounded-full hover:bg-background/50 transition-all' : undefined; return ( ); } /** Returns a human-readable description for the currently selected engine. */ export function getEngineDescription(engine: string): string { return ENGINE_DESCRIPTIONS[engine] ?? ''; } /** * Check if a profile is compatible with the currently selected engine. * Useful for UI hints. */ export function isProfileCompatibleWithEngine( profile: VoiceProfileResponse, engine: string, ): boolean { const voiceType = profile.voice_type || 'cloned'; if (voiceType === 'preset') return profile.preset_engine === engine; if (voiceType === 'cloned') return CLONING_ENGINES.has(engine); return true; // designed — future }