# 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.