DocsRechercheJustification des choix

Justification des Choix

Documentation détaillée des décisions architecturales et techniques du projet THOR, avec métriques comparatives et justifications basées sur les benchmarks.

Architecture Pipeline

Flux de données

Audio
WAV / WebM
STT
Whisper
NLP
spaCy fine-tuné
Pathfinding
Dijkstra
Entrée
audio_file
STTResult
text, confidence
NLPExtraction
origin, destination
Route
steps[], total_time

Principes architecturaux

Séparation modulaire

Interfaces abstraites STTModel, NLPModel, PathfindingModel

  • Test unitaire de chaque module en isolation
  • Échange de modèles sans modifier le code client
  • Complexité divisée par 3 (debug facilité)

Registry dynamique

ModelRegistry avec lazy loading des modèles

  • Startup rapide (~200ms vs ~3s sans lazy loading)
  • Configuration YAML sans recompilation
  • Ajout de nouveaux modèles = 1 décorateur

Types standardisés

Dataclasses STTResult, NLPExtraction, Route

  • Contrat explicite entre modules
  • Validation automatique des données
  • Sérialisation JSON native (API)

Validation en couches

TranscriptValidator, CityValidator, ExtractionValidator

  • Détection précoce des erreurs
  • Suggestions de correction automatiques
  • Métriques de confiance explicables

Système de Registry

src/common/registry.py
class ModelRegistry:
    _registry = {
        "stt": {
            "whisper": WhisperModel,      # GPU optionnel
            "vosk": VoskModel,            # CPU only
            "dummy": DummySTTModel
        },
        "nlp": {
            "spacy_finetuned": SpacyNLPModel,   # ◀ CHOISI
            "regex_advanced": RegexAdvancedModel,
            "transformers": TransformersNERModel
        },
        "pathfinding": {
            "dijkstra": DijkstraModel     # Temps pondéré
        }
    }
    
    @classmethod
    def get(cls, module_type: str, model_name: str):
        return cls._registry[module_type][model_name]

Choix STT : Whisper vs Vosk

Comparaison des processus

Whisper (small)

Choisi
Audio (tout format)
librosa → 16kHz mono
Transformer Encoder-Decoder
39M → 1.5B paramètres
"Je veux aller de Paris."
Ponctuation + Majuscules

Vosk

Testé
Audio (WAV 16kHz uniquement)
⚠ Format strict
Chunks de 4000 samples
Kaldi (CTC + HMM-GMM)
Modèle français pré-entraîné
"je veux aller de paris"
Texte brut uniquement

Métriques comparatives (30 échantillons)

Whisper (small)Choisi
WER (erreur mots)
29.13%
CER (erreur caractères)
12.24%
Transcriptions parfaites
16.7%
Latence moyenne
387ms
VoskTesté
WER (erreur mots)
48.12%
CER (erreur caractères)
16.05%
Transcriptions parfaites
0%
Latence moyenne
118ms

Justification du choix Whisper

1. Précision critique pour le NLP

Les erreurs STT se propagent au NLP : une erreur de transcription peut rendre l'extraction d'entités impossible.

WER 29% vs 48% = 40% moins d'erreurs
transmises au module NLP
2. Transcriptions parfaites

Seul Whisper produit des transcriptions sans aucune erreur.

Whisper: 16.7% parfaites (5/30)
Vosk: 0% parfaites (0/30)
3. Post-traitement automatique

Whisper ajoute ponctuation et majuscules, facilitant la détection des noms propres.

"Je veux aller de Paris à Lyon."
"je veux aller de paris à lyon"
4. Trade-off latence acceptable

387ms reste bien inférieur au seuil de perception humaine (~500ms).

RTF = 0.16 (6× plus rapide que temps réel)
GPU optionnel pour applications critiques

Choix NLP : spaCy fine-tuné vs Regex vs Transformers

Comparaison des approches d'extraction

Regex Advanced

Testé
"Je veux aller de Paris à Lyon"
100+ patterns prédéfinis
/depuis (\w+)/, /de (\w+) à/
Match dans liste de ~100 villes
Hardcodé, non extensible
origin="Paris", dest="Lyon"
⚠ Limité aux patterns connus

spaCy fine-tuné

Choisi
"Je veux aller de Paris à Lyon"
Tokenization + POS tagging
Analyse syntaxique complète
Embeddings fr_core_news_md
40MB, contexte sémantique
NER: labels ORIGIN + DESTINATION
Fine-tuné 20 iter, dropout 0.1
origin="Paris", dest="Lyon"
✓ Apprend de nouveaux patterns

Processus d'entraînement spaCy

Données annotées
JSONL, 438 samples
Modèle de base
fr_core_news_md
Modèle fine-tuné
ORIGIN + DESTINATION
Configuration
n_iter: 20
dropout: 0.1
batch_size: 4 → 32 (compounding)
labels: ["ORIGIN", "DESTINATION"]
Processus
1. Charger fr_core_news_md
2. Ajouter labels ORIGIN/DESTINATION au NER
3. Créer Examples à partir du JSONL
4. 20 itérations SGD avec validation

Métriques comparatives (test_nlp.jsonl)

ModèleF1-ScoreOrigineDestinationLes 2Status
spaCy fine-tuné0.62194.98%83.90%83.68%Choisi
regex_advanced0.43080.82%29.57%27.05%Testé
spaCy (base)0.40772.60%42.24%40.75%Testé
Transformers (CamemBERT)Erreur---Non retenu

Justification du choix spaCy fine-tuné

1. Amélioration F1 de +53%

Le fine-tuning permet au modèle d'apprendre les labels ORIGIN/DESTINATION spécifiques à notre cas d'usage.

Regex: F1 = 0.43
spaCy finetuned: F1 = 0.62 (+53%)
2. Précision destination corrigée

La regex échoue sur les destinations complexes ou implicites.

Regex: 29.57% destinations
spaCy: 83.90% (+183%)
3. Compréhension contextuelle

spaCy comprend le contexte sémantique, pas seulement les patterns.

"de Paris" → ORIGIN
"le Paris-Brest express" → ignoré
4. Pourquoi pas Transformers ?

CamemBERT a échoué sur notre dataset de test.

• Erreur à l'exécution
• Empreinte mémoire 10× supérieure (~400MB)
• Nécessite GPU pour latence acceptable
Pourquoi le Regex est insuffisant
Cas non gérés :
  • • Typos : "parir" → Paris non reconnu
  • • Destinations implicites : "vers chez moi"
  • • Structures complexes : "en passant par Avignon"
  • • Variations régionales : "sur Marseille"
Limites structurelles :
  • • ~100 villes hardcodées seulement
  • • Recall 39% vs 62% (−37%)
  • • Pas d'apprentissage possible
  • • Maintenance manuelle des patterns

Choix Pathfinding : Dijkstra avec pénalités

Algorithme de Dijkstra

Complexité temps
O((V + E) log V)
Nœuds (gares)
2,782
Arêtes (liaisons)
7,852
Pseudo-code Dijkstra avec pénalités
function dijkstra(graph, origin, destination):
    distances = {node: ∞ for all nodes}
    distances[origin] = 0
    parent = {}
    priority_queue = [(0, origin)]
    
    while priority_queue not empty:
        current_dist, current = pop_min(priority_queue)
        
        if current == destination:
            return reconstruct_path(parent)
        
        for neighbor, edge_data in graph.neighbors(current):
            # ◀ PÉNALITÉ : temps × multiplicateur selon type de train
            weight = edge_data.temps_min × PENALTY[edge_data.type_train]
            
            new_dist = current_dist + weight
            if new_dist < distances[neighbor]:
                distances[neighbor] = new_dist
                parent[neighbor] = current
                push(priority_queue, (new_dist, neighbor))
    
    return None  # Pas de chemin trouvé

Système de pénalités par type de train

Type de trainPénalitéJustification
TGV, OUIGO, Lyria, Eurostar×1.0Priorité maximale : rapide, confortable
Intercités×1.3+30% pour favoriser TGV quand disponible
Train de nuit×1.5Lent mais utile pour longues distances
TER, Navette×2.0Régional, nombreux arrêts
Auto-train×2.5Très lent, cas spécifique
Correspondance (métro)×1.0Transfert inter-gare, neutre

Exemple : Paris → Lyon

TGV◀ CHOISI
120 min × 1.0 = 120
TERNon retenu
180 min × 2.0 = 360

Construction du graphe depuis les données SNCF

stop_times.txt
Horaires réels GTFS
dataset_gares.json
UIC, GPS, noms
shapes.json
Géométries GeoJSON
scripts/generate_enhanced_graph.py
Extraction :
  • • Codes UIC (8 chiffres) via regex
  • • Arrêts consécutifs par trip_id
  • • Type train : OCETGV, OCEOUIGO, OCETER...
Agrégation par liaison :
  • • temps_moyen_min : moyenne observée
  • • temps_min_min : minimum observé
  • • nb_trains : fréquence journalière
NetworkX Graph
2,782 nœuds × 7,852 arêtes
Poids = temps (min) × pénalité

Gestion des villes multi-gares

Système de scoring

Nom exact+200
Préfixe ville + station+100
Gare principale (Part-Dieu, St-Charles)+50
Station TGV+30
Par connexion+2
Aéroport / banlieue-20

Exemple : Paris

Paris Gare de Lyon+350
Paris Montparnasse+340
Paris Nord+320
Paris Bercy+180
Paris CDG Aéroport+80

Justification du choix Dijkstra

1. Optimalité garantie

Dijkstra trouve toujours le chemin de poids minimal. Crucial pour un système de recommandation de trajets.

2. Pourquoi pas A* ?

A* nécessite une heuristique admissible.

Problème : temps ≠ distance euclidienne
→ Heuristique invalide → perte d'optimalité
3. Optimisation par le temps

Les utilisateurs optimisent leur temps de trajet, pas les kilomètres. Le système de pénalités encode cette préférence.

4. Performance suffisante

Avec 2,782 nœuds et 7,852 arêtes :

Temps moyen: <50ms par recherche
Correspondances inter-gares Paris

Arêtes spéciales pour les transferts métro entre gares parisiennes :

Montparnasse ↔ Lyon
~15 min
Lyon ↔ Nord
~15 min
Montparnasse ↔ Nord
~20 min

Données et prétraitement

Pipeline de données complète

1. SOURCES BRUTES
GTFS SNCF
stop_times.txt, stops.txt
Horaires officiels
OpenData Gares
dataset_gares.csv
2,782 gares France
Géométries
shapes.json
LineString GeoJSON
2. SCRIPTS DE TRAITEMENT
generate_enhanced_graph.py
  • • Extraction codes UIC depuis stop_id
  • • Calcul temps trajet entre arrêts consécutifs
  • • Identification type train (TGV, TER, etc.)
train_station_converter.py
  • • Normalisation noms de villes
  • • Construction aliases (Saint/St, etc.)
  • • Index ville → [gares]
3. DONNÉES PRODUITES
dataset_liaisons_enhanced.json
7,852 liaisons
temps min/moy/max, type train
dataset_gares.json
2,782 gares
UIC, GPS, commune
dataset_villes.json
Aliases + index
Recherche fuzzy

Données SNCF officielles

Source de vérité pour les temps de trajet

  • Horaires réels (pas estimations)
  • Couverture nationale exhaustive
  • Mise à jour régulière (GTFS)

Optimisation par le temps

Métrique alignée avec les besoins utilisateurs

  • Temps de trajet = critère principal
  • Distance euclidienne non représentative
  • TGV Paris-Lyon plus rapide malgré détours

Résumé des choix finaux

STT
Whisper (small)
WER 29% | 387ms latence
NLP
spaCy fine-tuné
F1 0.62 | 84% destination
Pathfinding
Dijkstra + pénalités
<50ms | Optimal garanti