import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { useAuth } from '../lib/auth'; import { useTenant } from '../lib/tenant'; import { api } from '../lib/api'; import { useToast } from '../hooks/useToast'; import { Upload, Database, Save, Activity, CheckCircle, AlertTriangle, RefreshCw, Lightbulb } from 'lucide-react'; import { logError } from '../lib/logger'; interface TrainingData { id: string; audioUrl: string; transcription: string; // generated by Whisper manualCorrection?: string; rawWER?: number; normalizedWER?: number; status: string; createdAt: string; } export default function TrainingLab() { const { t } = useTranslation(); const toast = useToast(); const { token, logout } = useAuth(); const { selectedOrgId } = useTenant(); const [mode, setMode] = useState<'db' | 'upload' | 'suggestions'>('db'); const [audios, setAudios] = useState([]); const [selectedAudio, setSelectedAudio] = useState(null); const [manualCorrection, setManualCorrection] = useState(''); const [loading, setLoading] = useState(false); const [submitting, setSubmitting] = useState(false); const [result, setResult] = useState<{ rawWER: number, normalizedWER: number, missingWords: string[] } | null>(null); const [suggestions, setSuggestions] = useState<{ original: string, replacement: string, count: number }[]>([]); const [selectedSuggestions, setSelectedSuggestions] = useState>(new Set()); const [recalculating, setRecalculating] = useState(false); const [recalcResult, setRecalcResult] = useState<{ processed: number, avgRawWER: number, avgNormalizedWER: number, improvementPercent: number } | null>(null); const fetchAudios = async () => { if (!token || !selectedOrgId) return; setLoading(true); try { const data = await api.get('/v1/admin/training/audios', token, selectedOrgId); setAudios(data.pending ?? data ?? []); } catch (err: any) { logError(err); } finally { setLoading(false); } }; useEffect(() => { if (mode === 'db') { fetchAudios(); } }, [mode, token, selectedOrgId, logout]); const handleSubmit = async () => { if (!selectedAudio || !manualCorrection.trim()) return; setSubmitting(true); setResult(null); try { const json = await api.post('/v1/admin/training/submit', { id: selectedAudio.id, audioUrl: selectedAudio.audioUrl, transcription: selectedAudio.transcription, manualCorrection }, token, selectedOrgId); setResult({ rawWER: json.rawWER, normalizedWER: json.normalizedWER, missingWords: json.missingWords ?? [] }); // Remove from list setAudios(prev => prev.filter(a => a.id !== selectedAudio.id)); } catch (err: any) { logError(err); toast.error(err.message || 'Erreur serveur.'); } finally { setSubmitting(false); } }; const fetchSuggestions = async () => { setLoading(true); try { const data = await api.get('/v1/admin/training/suggestions', token, selectedOrgId); setSuggestions(data); setSelectedSuggestions(new Set(data.map((d: any) => `${d.original}->${d.replacement}`))); } catch (err: any) { logError(err); } finally { setLoading(false); } }; const applySuggestions = async () => { const payload = suggestions.filter(s => selectedSuggestions.has(`${s.original}->${s.replacement}`)); if (payload.length === 0) return; setSubmitting(true); try { const json = await api.post('/v1/admin/training/apply-suggestions', { suggestions: payload }, token, selectedOrgId); toast.success(`Succès ! ${json.injectedCount} règles ont été injectées dans le dictionnaire.`); fetchSuggestions(); } catch (err: any) { logError(err); } finally { setSubmitting(false); } }; const recalculateWER = async () => { setRecalculating(true); try { const json = await api.post('/v1/admin/training/recalculate-wer', {}, token, selectedOrgId); setRecalcResult(json); } catch (err: any) { logError(err); } finally { setRecalculating(false); } }; return (

{t('training.title')}

{mode === 'suggestions' && (

Auto-Normalisation (Top 20)

Ces mots ont été fréquemment corrigés manuellement. Validez-les pour les injecter dans le dictionnaire Wolof.

{recalcResult && (

Benchmark Terminé ({recalcResult.processed} audios)

WER Brut Moy

{Math.round(recalcResult.avgRawWER * 100)}%

WER Normalisé Moy

{Math.round(recalcResult.avgNormalizedWER * 100)}%

Gain de Précision

+{recalcResult.improvementPercent.toFixed(2)}%

)} {loading ?

: suggestions.length === 0 ? (

Aucune nouvelle suggestion détectée.

) : (
{suggestions.map(s => { const key = `${s.original}->${s.replacement}`; return ( ); })}
setSelectedSuggestions(e.target.checked ? new Set(suggestions.map(s => `${s.original}->${s.replacement}`)) : new Set())} /> Erreur (Whisper) Correction (Humain) Fréquence
{ const newSet = new Set(selectedSuggestions); if (e.target.checked) newSet.add(key); else newSet.delete(key); setSelectedSuggestions(newSet); }} /> {s.original} {s.replacement} {s.count} fois
)}
)} {/* Left Sidebar: List */}

File d'attente ({audios.length})

{loading &&

Chargement...

} {!loading && audios.length === 0 && (

Aucun audio en attente de révision.

)} {audios.map(audio => ( ))}
{/* Right Area: Editor */}
{mode === 'upload' && !selectedAudio && (

Upload Manuel (Bientôt disponible)

Uploadez un fichier .wav/.mp3 pour le transcrire et l'ajouter au dataset d'entraînement local.

)} {selectedAudio && (

Audio Source

Whisper Genération v1

{selectedAudio.transcription}

Vérité Terrain (Ground Truth) Wolof Standardisé