import { useState, useEffect } from 'react'; import { useAuth } from '../App'; import { Upload, Database, Save, Activity, CheckCircle, AlertTriangle, RefreshCw, Lightbulb } from 'lucide-react'; const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:3001'; 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 { apiKey, logout } = useAuth(); 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 () => { setLoading(true); try { const res = await fetch(`${API_URL}/v1/admin/training/audios`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (res.status === 401) return logout(); const data = await res.json(); setAudios(data); } catch (err) { console.error(err); } finally { setLoading(false); } }; useEffect(() => { if (mode === 'db') { fetchAudios(); } }, [mode, apiKey, logout]); const handleSubmit = async () => { if (!selectedAudio || !manualCorrection.trim()) return; setSubmitting(true); setResult(null); try { const res = await fetch(`${API_URL}/v1/admin/training/submit`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: selectedAudio.id, audioUrl: selectedAudio.audioUrl, transcription: selectedAudio.transcription, manualCorrection }) }); if (res.status === 401) return logout(); const json = await res.json(); if (json.error) { alert('Erreur: ' + JSON.stringify(json.error)); } else { setResult({ rawWER: json.rawWER, normalizedWER: json.normalizedWER, missingWords: json.missingWords }); // Remove from list setAudios(prev => prev.filter(a => a.id !== selectedAudio.id)); } } catch (err) { console.error(err); alert('Erreur serveur.'); } finally { setSubmitting(false); } }; const fetchSuggestions = async () => { setLoading(true); try { const res = await fetch(`${API_URL}/v1/admin/training/suggestions`, { headers: { 'Authorization': `Bearer ${apiKey}` } }); if (res.status === 401) return logout(); const data = await res.json(); setSuggestions(data); setSelectedSuggestions(new Set(data.map((d: any) => `${d.original}->${d.replacement}`))); } catch (err) { console.error(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 res = await fetch(`${API_URL}/v1/admin/training/apply-suggestions`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ suggestions: payload }) }); if (res.status === 401) return logout(); const json = await res.json(); alert(`Succès! ${json.injectedCount} règles ont été injectées dans le dictionnaire.`); fetchSuggestions(); } catch (err) { console.error(err); } finally { setSubmitting(false); } }; const recalculateWER = async () => { setRecalculating(true); try { const res = await fetch(`${API_URL}/v1/admin/training/recalculate-wer`, { method: 'POST', headers: { 'Authorization': `Bearer ${apiKey}` } }); if (res.status === 401) return logout(); const json = await res.json(); setRecalcResult(json); } catch (err) { console.error(err); } finally { setRecalculating(false); } }; return (

Training Lab (WER)

{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é