# Fixtures SentencePiece — découpage de référence Ce dossier contient les **fixtures de référence** du tokeniseur SentencePiece maison (`src/Omika.App.Local/Services/SentencePieceTokenizer.cs`). ## À quoi ça sert Le tokeniseur est réécrit à la main : lecture protobuf minimale, Viterbi UNIGRAM, BPE, repli octet, normalisation. Son risque n'est pas de lever une exception — c'est de découper **légèrement autrement** que le modèle, et donc de dégrader les traductions **sans aucun signal** : pas de plantage, pas de message, juste des phrases un peu plus mauvaises. Une fixture transforme ce risque en mesure : pour une série de textes, elle fige ce que produit la bibliothèque `sentencepiece` de référence. ```json { "modele": "vocab.spm", "sentencepieceVersion": "0.2.0", "vocabSize": 32000, "addDummyPrefix": true, "removeExtraWhitespaces": true, "escapeWhitespaces": true, "normalizerName": "nmt_nfkc", "cas": [ { "texte": "Bonjour le monde.", "ids": [1, 2, 3], "decode": "Bonjour le monde." } ] } ``` - `modele` : nom du fichier `.spm` décrit (modèle cherché dans ce dossier) ; - `sentencepieceVersion` : version de la référence — elle **date** la comparaison, pour qu'une divergence constatée dans six mois ne se confonde pas avec un changement de version de la bibliothèque ; - `vocabSize`, `addDummyPrefix`, `removeExtraWhitespaces`, `escapeWhitespaces`, `normalizerName` : les réglages du modèle qui décident du découpage, lus dans son `normalizer_spec` — c'est la première chose à comparer quand un découpage diverge ; - `cas` : `ids` vient de `EncodeAsIds(texte)`, `decode` de `DecodeIds(ids)` appliqué à **ces** identifiants. La fixture est donc auto-cohérente : le test compare le tokeniseur C# à la référence, jamais à une transcription manuelle qui figerait une erreur de recopie ; - un fichier par modèle, nommé `.fixtures.json`, UTF-8 **sans BOM**, indenté. Les textes couverts sont construits par le script et comprennent, au minimum : français accentué, espagnol accentué avec `¿¡`, espaces de tête, de queue et doubles, chaîne vide, chaîne d'espaces seuls, chiffres et ponctuation, phrase très longue, casse mêlée, et une écriture non latine (arabe et cyrillique). ## Générer les fixtures Prérequis : Python 3 et la bibliothèque de référence. ```powershell pip install sentencepiece .\scripts\Generer-Fixtures-SentencePiece.ps1 -DossierModeles C:\Chemin\Vers\Les\Modeles ``` Le générateur cherche **récursivement** tous les `*.spm` sous `-DossierModeles` et écrit un fixture par modèle dans `tests\fixtures\sentencepiece` (ou dans le dossier passé à `-Sortie`). Relancer le script est sans danger : il réécrit ses propres fixtures et ne supprime rien d'autre. Options utiles : ```powershell # Copie chaque .spm à côté de son fixture : le test cherche le modèle ici. .\scripts\Generer-Fixtures-SentencePiece.ps1 -DossierModeles C:\Modeles -CopierModeles # Autre dossier de sortie (utile pour un pack non versionné). .\scripts\Generer-Fixtures-SentencePiece.ps1 -DossierModeles C:\Modeles -Sortie C:\Temp\fixtures ``` ## Où trouver un `.spm` Le vocabulaire est livré **dans chaque pack de langue**, un fichier par sens de traduction : ``` langues/{code}/{sens}/vocab.spm # {code} = es, pt, ar… {sens} = versFrancais, depuisFrancais ``` Ce chemin est celui du stockage local de l'application (voir `LanguagePaths` dans `src/Omika.App.Local/Infrastructure`). Deux façons de se procurer le fichier : 1. le récupérer depuis le poste où le pack a été téléchargé (dossier de données de l'application, sous `langues/`) ; 2. le demander à l'API des langues, qui le sert avec le reste du pack. Un même dossier `langues/{code}/{sens}` peut donc servir de `-DossierModeles`. ## Les `.spm` ne sont pas versionnés Les fichiers `.spm` (comme les `encoder.onnx`) restent **hors du dépôt** : - ils pèsent plusieurs centaines de kilo-octets à plusieurs méga-octets chacun, multipliés par le nombre de langues et de sens ; - ce sont des **artefacts de modèle**, produits par un entraînement et soumis à leurs propres conditions de licence : leur place est dans un pack de langue distribué, pas dans l'historique Git. Le générateur ne versionne que les **fixtures** (quelques kilo-octets de texte), qui décrivent le comportement attendu sans embarquer le modèle. ## Comment le test trouve tout ça `tests/Omika.Tests/Mobile/SentencePieceFixturesTests.cs` : 1. dossier des fixtures : la variable d'environnement `OMIKA_SPM_FIXTURES` si elle est définie et existe, sinon `tests/fixtures/sentencepiece` **résolu depuis la racine du dépôt** (racine trouvée en remontant depuis l'assembly de test jusqu'au dossier contenant `Omika.slnx` — aucun chemin absolu codé en dur, aucun recours au dossier courant) ; 2. pour chaque `*.fixtures.json`, le `.spm` annoncé par `modele` est cherché dans ce même dossier (nom simple, ou sous-dossier annoncé par la fixture) ; 3. si le `.spm` est **absent**, la fixture est ignorée — jamais un échec ; 4. si le dossier est **absent ou vide**, le test **passe** en expliquant pourquoi sur la sortie standard. C'est délibéré : une suite rouge sur une machine fraîchement clonée, pour de simples fichiers absents, pousserait à désactiver le test — exactement l'inverse du but. Ces tests ne font échouer que sur une divergence **réellement mesurée**. Pour vérifier un pack sans rien copier dans le dépôt : ```powershell $env:OMIKA_SPM_FIXTURES = 'C:\Chemin\Vers\fixtures-et-spm' dotnet test tests\Omika.Tests\Omika.Tests.csproj --filter FullyQualifiedName~SentencePieceFixturesTests ``` Deux autres tests de ce fichier s'exécutent même sans fixture : l'idempotence de l'aller-retour (`Decode(Encode(x))` rejoué donne le même texte — jamais `Decode(Encode(x)) == x`, qui dépend du vocabulaire), et le fait qu'un fichier inexploitable lève une `SentencePieceException` **en français** plutôt qu'une erreur système. ## Lire un échec Le message d'échec est conçu pour être exploitable seul : ``` Découpage divergent pour le cas « Bonjour le monde. » (fixture vocab.fixtures.json). Premier écart à l'index 1 : référence 2537 « le », tokeniseur 12 « ▁le ». Identifiants de référence (4) : 1, 2537, 232, 4 Identifiants obtenus (4) : 1, 12, 232, 4 Texte reconstruit par la référence : « Bonjour le monde. ». ``` Ce qu'il faut y lire : - **l'index du premier écart** : les identifiants qui précèdent sont corrects, la divergence commence là. Un écart à l'index 0 est un problème de préfixe factice ou de normalisation ; un écart en milieu de phrase est un problème de segmentation (Viterbi, scores, longueur maximale de pièce) ; - **les deux identifiants et leur pièce** : `12` contre `2537` dit *quelle* pièce le tokeniseur a choisie à la place de celle du modèle. Une pièce très courte (`▁`, un caractère) à la place d'une pièce longue signale une table de pièces incomplète ou un score mal lu ; l'inverse signale un score trop favorable ; - **les deux listes** : des tailles différentes signalent une couverture incomplète (caractère devenu `` au lieu d'une pièce) ; - **`decode`** : « Décodage divergent » n'a rien à voir avec le découpage — le décodage est testé sur les identifiants **de la référence**, donc l'erreur est dans la reconstruction (marqueur ▁, rognage des bords, reconstitution des octets) ; - les réglages en tête de fixture (`addDummyPrefix`, `normalizerName`…) sont à comparer avec `ProfilNormalisation` et `Avertissements` du tokeniseur chargé. Vérifier aussi que la fixture et le modèle vont bien ensemble : le test échoue explicitement si la taille de vocabulaire de la fixture ne correspond pas à celle du `.spm` (mauvais appariement de fichiers, pas divergence de segmentation). ## Que faire d'une divergence **La signaler. Ne jamais l'absorber.** - Ne **pas** desserrer la valeur attendue, ne **pas** régénérer la fixture « jusqu'à ce que ça passe », ne **pas** supprimer le cas gênant : une fixture réécrite pour coller au tokeniseur ne mesure plus rien, et la divergence repartira en production sous forme de traductions dégradées. - Ouvrir une anomalie avec : le nom de la fixture, le texte du cas, l'index du premier écart, les deux identifiants et les deux pièces, les deux listes, la version `sentencepieceVersion`, et la taille du vocabulaire. - Regarder d'abord les **limites assumées** documentées dans l'en-tête de `SentencePieceTokenizer.cs` : la table de normalisation compilée (`precompiled_charsmap`) n'est pas appliquée, et les pièces `USER_DEFINED` sont traitées comme des pièces ordinaires. Une divergence sur un texte en pleine largeur (`123`), une ligature (`fi`), un exposant ou une forme de compatibilité vient très souvent de là — la propriété `Avertissements` du tokeniseur le dit déjà quand c'est le cas. En revanche `add_dummy_prefix`, `escape_whitespaces`, `remove_extra_whitespaces` et le **repli octet** sont lus dans le modèle et honorés : une divergence sur ces points n'est plus une limite connue, c'est un défaut à corriger. - Vérifier en priorité les cas **« espaces seuls » et espaces de bord** : le `Decode` du tokeniseur ne retire que le préfixe factice qu'il a lui-même ajouté, alors que la référence applique sa propre règle de rognage. C'est le premier écart susceptible d'apparaître sur un run réel. - Corriger le **tokeniseur** (avec un test qui reproduit le cas), jamais la fixture. Si la référence elle-même change de comportement d'une version à l'autre, mettre à jour `sentencepieceVersion` et justifier l'écart dans le message de commit. ## Fichiers de ce dossier | Fichier | Contenu | | --- | --- | | `LISEZMOI.md` | ce document | | `.fixtures.json` | fixtures de référence, régénérables, versionnées | | `*.spm` | **non versionnés** — modèles, à copier sur le poste de test |