'use client'; import { useState, useCallback, useMemo } from 'react'; import { Volume2, Play, Loader2 } from 'lucide-react'; import { toast } from 'sonner'; import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/hooks/use-i18n'; import { useSettingsStore } from '@/lib/store/settings'; import { getTTSVoices } from '@/lib/audio/constants'; import { useTTSPreview } from '@/lib/audio/use-tts-preview'; import { getVoxCPMProviderOptions, getVoxCPMVoiceOptions, useVoxCPMVoiceProfiles, } from '@/lib/audio/voxcpm-voices'; import { VOXCPM_AUTO_VOICE_ID, normalizeVoxCPMBackend, voxCPMBackendSupportsReferenceAudio, } from '@/lib/audio/voxcpm'; /** Extract the English name from voice name format "ChineseName (English)" */ function getVoiceDisplayName( id: string, name: string, lang: string, t: (key: string) => string, ): string { if (id === VOXCPM_AUTO_VOICE_ID) { return t('settings.voxcpmAutoVoice'); } if (lang === 'en-US') { const match = name.match(/\(([^)]+)\)/); return match ? match[1] : name; } return name; } export function TtsConfigPopover() { const { t, locale } = useI18n(); const [open, setOpen] = useState(false); const { previewing, startPreview, stopPreview } = useTTSPreview(); const ttsEnabled = useSettingsStore((s) => s.ttsEnabled); const setTTSEnabled = useSettingsStore((s) => s.setTTSEnabled); const ttsProviderId = useSettingsStore((s) => s.ttsProviderId); const ttsVoice = useSettingsStore((s) => s.ttsVoice); const ttsSpeed = useSettingsStore((s) => s.ttsSpeed); const ttsProvidersConfig = useSettingsStore((s) => s.ttsProvidersConfig); const setTTSVoice = useSettingsStore((s) => s.setTTSVoice); const { profiles: voxcpmProfiles } = useVoxCPMVoiceProfiles(); const voxcpmBackend = normalizeVoxCPMBackend( ttsProvidersConfig['voxcpm-tts']?.providerOptions?.backend, ); const voices = ttsProviderId === 'voxcpm-tts' ? getVoxCPMVoiceOptions(voxcpmProfiles, { supportsClone: voxCPMBackendSupportsReferenceAudio(voxcpmBackend), }) : getTTSVoices(ttsProviderId); const localizedVoices = useMemo( () => voices.map((v) => ({ ...v, displayName: getVoiceDisplayName(v.id, v.name, locale, t), })), [voices, locale, t], ); const pillCls = 'inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium transition-all cursor-pointer select-none whitespace-nowrap border'; const canPreview = ttsProviderId !== 'voxcpm-tts' || ttsVoice !== VOXCPM_AUTO_VOICE_ID; const handlePreview = useCallback(async () => { if (previewing) { stopPreview(); return; } if (!canPreview) { toast.info(t('settings.voxcpmAutoVoiceNoPreview')); return; } try { const providerConfig = ttsProvidersConfig[ttsProviderId]; const providerOptions = ttsProviderId === 'voxcpm-tts' ? { ...(providerConfig?.providerOptions || {}), ...(await getVoxCPMProviderOptions(ttsVoice, { role: 'teacher', locale })), } : undefined; await startPreview({ text: t('settings.ttsTestTextDefault'), providerId: ttsProviderId, modelId: providerConfig?.modelId, voice: ttsVoice, speed: ttsSpeed, apiKey: providerConfig?.apiKey, // Managed providers resolve their base URL server-side; only send the // client's own base URL (custom providers). baseUrl: providerConfig?.baseUrl || providerConfig?.customDefaultBaseUrl, providerOptions, }); } catch (error) { const message = error instanceof Error && error.message ? error.message : t('settings.ttsTestFailed'); toast.error(message); } }, [ previewing, canPreview, startPreview, stopPreview, t, locale, ttsProviderId, ttsProvidersConfig, ttsSpeed, ttsVoice, ]); const handleOpenChange = useCallback( (nextOpen: boolean) => { if (!nextOpen) { stopPreview(); } setOpen(nextOpen); }, [stopPreview], ); return ( {t('toolbar.ttsHint')} {/* Header with toggle */}
{t('toolbar.ttsTitle')}
{/* Config body */} {ttsEnabled && (
{/* Voice + Preview row */}
)}
); }