# Valider la traduction ONNX — activation du moteur et mesure de la qualité > **Objet** : traiter les deux suites de la traduction hors ligne mobile : > (1) activer `OMIKA_ONNX_RUNTIME` et faire tourner une **vraie** paire FR⇄ES ; > (2) lever le doute sur le **tokeniseur** SentencePiece. > > **Cible** : poste de développement (accès NuGet) + un appareil Android. > **Prérequis** : `DEPLOIEMENT_API_LANGUES.md` (service serveur) et > `TRADUCTION_HORS_LIGNE_MOBILE.md` (côté application) déjà lus. --- ## 1. Les deux problèmes ne se traitent pas de la même façon | | Nature | Qui peut le faire | Coût | |---|---|---|---| | **Point 1** — activer le moteur | **Procédure d'exécution** : aucun code à écrire, 3 modifications de projet + une validation à exécuter | Toute machine avec accès NuGet | ~1 h | | **Point 2** — tokeniseur | **Travail de code** déjà largement fait (§3.1), puis **mesure** : on ne corrige plus à l'aveugle, on compare à la référence | Développeur | 2 h pour mesurer, variable ensuite | > **Règle directrice pour le point 2** : *ne jamais « améliorer » le tokeniseur > sans mesure*. Une normalisation devinée est **pire** que pas de normalisation : > elle produit un découpage différent de la référence de façon invisible. On > mesure d'abord (§3.3), on corrige ensuite — et seulement ce qui diverge. --- ## 2. Point 1 — activer le moteur et faire tourner une vraie paire FR⇄ES ### 2.1 Prérequis ```powershell # Vérifier que NuGet est joignable (c'est le seul prérequis bloquant) dotnet nuget list source # Un pack de langue de test (voir §2.3), et ~5 Go de disque libre ``` ### 2.2 Étape A — activer le moteur (3 modifications, aucune ligne de code) 1. `Directory.Packages.props` : ```xml ``` 2. `src/Omika.App.Local/Omika.App.Local.csproj` : ```xml $(DefineConstants);OMIKA_ONNX_RUNTIME ``` 3. `src/Omika.App/AppServices.cs` — une ligne : ```csharp IOnnxInferenceSessionFactory onnxFactory = OnnxRuntimeInferenceSessionFactory.Instance; ``` **Vérification immédiate** — les deux commandes doivent passer : ```powershell dotnet build src/Omika.App.Local/Omika.App.Local.csproj -c Debug dotnet build tools/Omika.LanguesProbe/Omika.LanguesProbe.csproj -c Debug ``` > `OnnxRuntimeInferenceSessionFactory.cs` est le **seul** fichier dont la > compilation dépend du paquet : c'est voulu. S'il ne compile pas, c'est une > incompatibilité d'API avec la version du paquet — corrigez ce fichier > uniquement, rien d'autre n'en dépend. ### 2.3 Étape B — obtenir une paire FR⇄ES réelle Les modèles de traduction ne sont pas livrés avec Omika (poids + licences). Le plus simple est un modèle **OPUS-MT** de Helsinki-NLP, exporté en ONNX. ```powershell pip install "optimum[onnxruntime]" transformers sentencepiece # fr -> es (sert le sens « depuisFrancais » du pack « es ») optimum-cli export onnx --model Helsinki-NLP/opus-mt-fr-es .\export\fr-es # es -> fr (sert le sens « versFrancais » du pack « es ») optimum-cli export onnx --model Helsinki-NLP/opus-mt-es-fr .\export\es-fr ``` L'export produit notamment : ``` encoder_model.onnx decoder_model.onnx decoder_with_past_model.onnx <-- NE PAS LIVRER source.spm target.spm ``` > ⚠️ **`decoder_with_past_model.onnx` ne doit jamais être livré dans le pack.** > C'est un décodeur à cache de clés/valeurs (`past_key_values`) destiné à la > génération incrémentale : il attend des entrées que ce pipeline ne fournit pas. > `OnnxTranslationService` l'exclut désormais explicitement (nom contenant > `past`), mais autant ne pas le copier. > ⚠️ **Un modèle Marian a DEUX vocabulaires** : `source.spm` (langue d'entrée) et > `target.spm` (langue de sortie). Les livrer **tous les deux** est obligatoire — > voir §2.4. ### 2.4 Étape C — construire le pack local > Cette étape construit un pack **de test sur le poste**. Pour livrer un pack en > **production** (dépôt sous `Langues__RootPath`, génération du manifeste, > contrôles), la procédure dédiée est `LIVRAISON_MODELES_LANGUES.md` — l' > arborescence et les règles de nommage y sont les mêmes. Arborescence à créer, par exemple dans `C:\Omika\TestLangues\es` : ``` es/ ├── versFrancais/ (es -> fr) │ ├── encoder_model.onnx <- depuis .\export\es-fr\ │ ├── decoder_model.onnx <- depuis .\export\es-fr\ │ ├── source.spm <- depuis .\export\es-fr\source.spm (ESPAGNOL) │ └── target.spm <- depuis .\export\es-fr\target.spm (FRANÇAIS) └── depuisFrancais/ (fr -> es) ├── encoder_model.onnx <- depuis .\export\fr-es\ ├── decoder_model.onnx <- depuis .\export\fr-es\ ├── source.spm <- depuis .\export\fr-es\source.spm (FRANÇAIS) └── target.spm <- depuis .\export\fr-es\target.spm (ESPAGNOL) ``` **Règles de nommage** (résolues automatiquement par le service, aucune configuration nécessaire) : | Fichier | Rôle | Repli si absent | |---|---|---| | `source.spm` | vocabulaire d'entrée | `vocab.spm`, puis premier `.spm` | | `target.spm` | vocabulaire de sortie | `vocab.spm`, puis premier `.spm` | | `vocab.spm` | vocabulaire **commun** aux deux langues (NLLB, mBART, M2M-100) | premier `.spm` | | `encoder_model.onnx` / `encoder.onnx` / tout autre `.onnx` | modèle principal | — | | `decoder_model.onnx` / `decoder.onnx` | décodeur séparé | mode « modèle unique » | | `*_with_past*.onnx` | **ignoré** | — | > **Modèle à vocabulaire partagé** (NLLB, mBART) : un seul `vocab.spm` suffit, > il sert aux deux sens. **Modèle Marian/OPUS-MT** : `source.spm` + `target.spm` > sont obligatoires. Les noms d'entrée du graphe (`input_ids` vs > `decoder_input_ids`, `encoder_hidden_states` vs `memory`) sont eux aussi > résolus automatiquement sur le modèle réellement chargé. ### 2.5 Étape D — valider sur le POSTE, sans téléphone C'est le cœur du traitement du point 1 : **on ne débogue pas un modèle de 120 Mo par déploiement d'APK.** ```powershell # 1) Le vocabulaire est-il lisible, et le tokeniseur cohérent ? dotnet run --project tools/Omika.LanguesProbe -- inspecter .\TestLangues\es\versFrancais\source.spm # 2) Que produit exactement la tokenisation ? (à comparer à la référence, §3.3) dotnet run --project tools/Omika.LanguesProbe -- encoder .\TestLangues\es\versFrancais\source.spm "Buenos días, ¿cómo está?" # 3) La traduction complète, avec les temps de chargement et d'inférence dotnet run --project tools/Omika.LanguesProbe -- traduire ` --dossier .\TestLangues\es\versFrancais --code es --sens versFrancais ` --texte "Sustitución de la pieza" ``` > **Pack familial** (modèle multilingue, §2.1.1 de > `LIVRAISON_MODELES_LANGUES.md`) : pointer `--dossier` sur le dossier du > **sens** (`langues/pt/versFrancais`). La commande lit les poids dans le > `commun/` voisin et affiche la ligne `Jeton cible : >>fra<<` — si cette ligne > manque, le pack produira la mauvaise langue sans aucune erreur. **Ce qu'il faut regarder, dans cet ordre :** 1. `inspecter` — `ProfilNormalisation`, `RepliOctetDisponible`, et surtout les **avertissements**. S'il annonce `precompiled_charsmap … NON appliquée`, c'est attendu (§3.2) : notez-le, ne le corrigez pas encore. 2. `inspecter` — les **aller-retours** `texte → ids → texte`. Tous doivent être `IDENTIQUE`. Un `DIFFERENT` sur du texte ordinaire est le symptôme d'un découpage divergent : passez au §3.3 **avant** de chercher ailleurs. 3. `traduire` — la sortie doit être une phrase **française plausible**. Pas parfaite : plausible. « Remplacement de la pièce » ✅. Une suite de mots espagnols, du texte répété ou des caractères isolés ❌ → voir §3.4. **Repères de performance à noter** (ils décideront de la faisabilité mobile) : | Mesure | Attendu sur poste | Seuil d'alerte | |---|---|---| | Chargement du modèle | < 2 s | > 5 s | | Inférence d'une phrase courte | < 1 s | > 3 s | | Aller-retour tokeniseur seul | < 5 ms | > 50 ms | ### 2.6 Étape E — valider sur Android ```powershell dotnet publish src/Omika.App.Android/Omika.App.Android.csproj -c Release -r android-arm64 ``` 1. Copier le dossier `es\` complet dans le stockage de l'appareil, sous `/Omika/langues/es/` — ou, plus réaliste, publier le pack sur le service Langues et le télécharger depuis **Paramètres → Langue secondaire** (c'est aussi le test du §3 de `TRADUCTION_HORS_LIGNE_MOBILE.md`). 2. **Avant de tester la traduction**, vérifier le poids : un modèle Marian fait ~300 Mo pour les deux sens. C'est la décision de livrable la plus lourde de tout le lot. 3. Dicter en espagnol dans une note interne → le champ doit recevoir du français. 4. Ouvrir le panneau de dictée → la bascule `⇄` doit redémarrer l'écoute en espagnol. 5. Sur une note existante, appuyer sur « ⇄ Traduire » → affichage espagnol, et **rien** ne doit apparaître dans l'Outbox (vérifier la page Synchronisation). ### 2.7 Critères go / no-go Le moteur est **validé** si et seulement si : - [ ] `inspecter` : aucun aller-retour `DIFFERENT` sur du texte ordinaire ; - [ ] `traduire` : phrases françaises plausibles sur 10 phrases variées (avec accents, chiffres, ponctuation, une phrase longue) ; - [ ] inférence < 3 s par phrase sur l'appareil Android cible ; - [ ] poids de l'APK mesuré et accepté ; - [ ] la dictée traduite insère du français, et **aucune** donnée traduite n'apparaît dans l'Outbox. Sinon → §3.4. ### 2.8 Rollback Retirer les 3 modifications du §2.2 (le symbole, le `PackageReference`, la ligne d'`AppServices`). L'application repasse à `UnavailableOnnxInferenceSessionFactory` : la traduction est annoncée indisponible, **rien d'autre ne change**. Aucune migration de données, aucun état à nettoyer — c'est précisément pourquoi le moteur est isolé derrière une fabrique. --- ## 3. Point 2 — le tokeniseur ### 3.1 Ce qui a été corrigé (et pourquoi c'était faux) Quatre défauts réels ont été corrigés. Aucun n'était visible sur les modèles les plus courants, ce qui les rendait d'autant plus dangereux : | Défaut | Conséquence avant correction | État | |---|---|---| | `normalizer_spec` ignoré : préfixe `▁` ajouté d'office | Un modèle formé avec `add_dummy_prefix=false` (fréquent sur les modèles récents) était découpé — donc traduit — **différemment** de la référence | ✅ lu et honoré | | Espaces multiples / de bord jamais réduits | `« Bonjour le monde »` produisait des marqueurs `▁` parasites absents de la référence | ✅ `remove_extra_whitespaces` honoré (espace, tabulation, `\n`, `\r`, `\v`, `\f`) | | `escape_whitespaces` supposé vrai | Un modèle à espaces littéraux était découpé de travers | ✅ honoré | | **Repli octet absent** | Sur un modèle multilingue, un caractère hors vocabulaire devenait `` : les accents et les alphabets non latins étaient **purement perdus** | ✅ implémenté (encodage **et** décodage) | | **Un seul vocabulaire par pack** | **Le défaut le plus grave** : un modèle Marian a un vocabulaire par langue. Décoder la sortie avec le vocabulaire d'entrée produit des identifiants interprétés dans le mauvais référentiel → du charabia | ✅ `source.spm` + `target.spm` | Deux points qui **n'étaient pas** des défauts, vérifiés : - la pénalité du jeton inconnu (`min_score − 10`) est bien celle de la référence (`kUnkPenalty = 10.0`) ; - un `` par **point de code** (et non par unité UTF-16) est correct : une paire de substituts ne doit jamais être coupée en deux. ### 3.2 Ce qui reste approximé — et l'impact attendu **`precompiled_charsmap` n'est pas appliqué.** C'est la table de normalisation Unicode compilée du modèle (structure de trie à double tableau, format propre à SentencePiece). La réimplémenter représenterait plusieurs centaines de lignes pour un gain **nul sur la majorité des textes**. Impact réel, à connaître : - **Aucun** sur du texte déjà normalisé en NFC (ce que produit tout clavier Android/iOS, et tout ce que l'API renvoie) ; - **Quelques cas** sur les formes de compatibilité (`fi` ligature, chiffres pleine largeur `123`, exposants `²`) et sur les langues dont l'écriture demande un repli de casse ou une normalisation de présentation. Le tokeniseur **le signale** désormais lui-même (`Avertissements`, `ProfilNormalisation`). C'est visible dans `inspecter` et dans les diagnostics. ### 3.3 Mesurer la divergence (avant de corriger quoi que ce soit) La référence est la bibliothèque Python `sentencepiece`, l'implémentation d'origine de Google. ```powershell pip install sentencepiece # Génère une fixture de référence par modèle trouvé (récursif) .\scripts\Generer-Fixtures-SentencePiece.ps1 -DossierModeles .\TestLangues ``` La fixture contient, pour chaque cas d'essai, les identifiants **de référence** et le décodage **de référence**. Le test `SentencePieceFixturesTests` les compare ensuite au tokeniseur Omika : ```powershell dotnet test tests/Omika.Tests --filter SentencePieceFixtures ``` - **Dossier de fixtures absent → le test passe** (sinon toute compilation sans modèle serait rouge). Les cas d'essai couvrent accents français, accents et `¿¡` espagnols, espaces de bord et doubles, chaîne vide, espaces seuls, chiffres et ponctuation, casse mixte, phrase longue, et un alphabet non latin. - **Un échec nomme le cas fautif**, le premier index divergent, les deux identifiants et le texte de la pièce : de quoi corriger sans deviner. - Les `.spm` ne sont **pas versionnés** (poids + licences) — voir `tests/fixtures/sentencepiece/LISEZMOI.md`. ### 3.4 Arbre de décision selon ce que révèle la mesure | Symptôme mesuré | Cause la plus probable | Action | |---|---|---| | Aller-retour `DIFFERENT` sur du texte **ordinaire** (accents, ponctuation) | Divergence de découpage | Comparer avec `encoder` : si les identifiants diffèrent dès le **premier**, c'est la normalisation ; s'ils divergent plus loin, c'est le Viterbi | | Divergence **uniquement** sur les cas de compatibilité (`fi`, `123`) | `precompiled_charsmap` non appliqué | **Accepter** et documenter : impact réel nul sur une dictée | | Traduction = suite de mots de la langue source, ou texte répété | Vocabulaire cible faux (ou absent) | Vérifier que `target.spm` est bien livré dans le sens concerné (§2.4) | | Traduction = ``, `▁`, ou caractères isolés | Mauvais vocabulaire, ou modèle non aligné sur le vocabulaire | Vérifier que `source.spm`/`target.spm` viennent bien du **même** modèle que les `.onnx` | | Erreur « n'expose aucune des entrées attendues (…) » | Noms d'entrée atypiques | Le message énumère les entrées réelles du modèle : renseigner `OnnxTranslationOptions` en conséquence | | Sortie entière ignorée / erreur de type | ONNX Runtime refuse un type inattendu | Corrigé : le type est testé **avant** `AsTensor` (qui lève au lieu de retourner `null`) | | Divergence **après** avoir tout vérifié | Vraie limite de l'implémentation | Ouvrir un ticket avec la fixture en échec **jointe** : elle contient tout ce qu'il faut pour reproduire | > **Ce qu'il ne faut PAS faire** : assouplir la fixture attendue pour faire > passer le test. Une fixture modifiée est une divergence rendue invisible. --- ## 4. Hors périmètre (à ne pas confondre) - **Qualité de traduction** : ce document valide que le *pipeline* fonctionne, pas que le modèle traduit bien. Un modèle médiocre restera médiocre — c'est un choix de modèle, pas un défaut de code. - **Poids du livrable mobile** : l'activation du moteur ONNX Runtime ajoute la bibliothèque native par architecture, et les modèles pèsent 80 à 300 Mo par langue. C'est une décision produit, pas technique. - **Reprise/streaming** du téléchargement : non implémenté (le re-téléchargement repart de zéro par fichier, mais les fichiers déjà conformes sont ignorés). - **Beam search** : le décodage est glouton (compromis de latence assumé). --- ## 5. Récapitulatif | # | Action | Qui | Où | |---|---|---|---| | 1 | Activer le symbole + le paquet + la fabrique (§2.2) | Dev (NuGet) | 3 fichiers | | 2 | Exporter une paire FR⇄ES en ONNX (§2.3) | Dev | poste | | 3 | Construire le pack `source.spm` + `target.spm` (§2.4) | Dev | disque | | 4 | `inspecter` / `encoder` / `traduire` (§2.5) | Dev | poste — **pas de téléphone** | | 5 | Générer les fixtures et lancer le test (§3.3) | Dev | `scripts\` + `dotnet test` | | 6 | Trancher avec l'arbre de décision (§3.4) | Dev | — | | 7 | Publier sur Android et mesurer poids + latence (§2.6) | Dev | appareil | | 8 | Décider go / no-go (§2.7) | Produit + Dev | — | **Si une seule chose doit être retenue** : les étapes 4 et 5 se font **sur le poste, en quelques minutes**. Tout le reste — déploiement d'APK, téléchargement de 300 Mo sur un téléphone, instrumentation à distance — est un détour.