import { useEffect } from 'react'; import { Download, Trash2, X, CheckCircle2, Cpu, Mic, Volume2, AlertTriangle, RefreshCw, Ear, Activity } from 'lucide-react'; import type { VoiceModelStatus } from '../../../shared/voice'; import { bridge } from '../../lib/bridge'; import { useShallow } from 'zustand/react/shallow'; import { pickSpeaker } from '../../lib/voicePreference'; import { useVoiceModels } from '../../state/voiceModels'; import { useSettings } from '../../state/settings'; function formatMb(bytes: number): string { return bytes >= 1024 * 1024 * 1024 ? `${(bytes / 1024 ** 3).toFixed(2)} Go` : `${Math.round(bytes / 1024 / 1024)} Mo`; } function ModelRow({ model }: { model: VoiceModelStatus }) { const progress = useVoiceModels((s) => s.progress[model.id]); const { download, cancel, remove } = useVoiceModels.getState(); const activeStt = useSettings((s) => s.settings.voice.localModel); const activeTts = useSettings((s) => s.settings.speech.localModel); const update = useSettings((s) => s.update); const isActive = model.kind === 'stt' ? activeStt === model.id : activeTts === model.id; const busy = !!progress && (progress.phase === 'download' || progress.phase === 'extract'); const wakeMode = useSettings((s) => s.settings.voice.wakeMode); const activate = () => { if (model.kind === 'stt') update({ voice: { localModel: model.id, provider: 'local' } }); else if (model.kind === 'kws') update({ voice: { wakeMode: 'kws' } }); else if (model.kind === 'vad') update({ voice: { neuralVad: true } }); else { const { speech } = useSettings.getState().settings; update({ speech: { localModel: model.id, provider: 'local', localSpeaker: pickSpeaker(model, speech.language || 'fr-FR', speech.voiceGender ?? 'male') } }); } }; const neuralVad = useSettings((s) => s.settings.voice.neuralVad); const activeNow = model.kind === 'kws' ? wakeMode === 'kws' : model.kind === 'vad' ? neuralVad : isActive; return (
{model.name} {model.recommended && recommandé} {model.installed && installé · {formatMb(model.installedBytes)}} {!model.installed && !busy && {model.sizeMb} Mo} {activeNow && model.installed && actif}
{model.description} langues : {model.languages.join(', ')}{model.speakers ? ` · ${model.speakers.length} voix` : ''} {busy && (
{progress.phase === 'extract' ? 'extraction…' : `${progress.percent}% · ${formatMb(progress.received)} / ${formatMb(progress.total)}`}
)} {progress?.phase === 'error' && échec : {progress.message}}
{busy ? ( ) : model.installed ? ( <> {!activeNow && } ) : ( )}
); } export function ModelsSection() { const { models, engine, error } = useVoiceModels(useShallow((s) => ({ models: s.models, engine: s.engine, error: s.error }))); const { refresh, checkEngine } = useVoiceModels.getState(); useEffect(() => { void refresh(); void checkEngine(); }, [refresh, checkEngine]); if (!bridge()) return
Les modèles locaux ne sont disponibles que dans l’application Electron.
; const stt = models.filter((m) => m.kind === 'stt'); const tts = models.filter((m) => m.kind === 'tts'); const kws = models.filter((m) => m.kind === 'kws'); const vad = models.filter((m) => m.kind === 'vad'); return ( <>
Moteur local (sherpa-onnx)
{engine === null ? ( vérification… ) : engine.available ? (
moteur prêt · version {engine.version}{engine.loaded.length ? ` · chargé : ${engine.loaded.join(', ')}` : ''}
) : (
{engine.error}
)} Les modèles tournent sur le processeur, sans connexion, dans un processus séparé. Dossier : {engine?.modelsDir} {error &&
{error}
}
Mot d’activation
{kws.map((m) => )}
Fin de phrase
{vad.map((m) => )}
Utilisé automatiquement en écoute permanente dès qu’il est installé ; sinon le VAD énergétique prend le relais.
Reconnaissance vocale
{stt.map((m) => )}
Synthèse vocale
{tts.map((m) => )}
); }