Files
2026-10-23 18:46:50 +02:00

213 lines
10 KiB
Markdown

# 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é `<nomDuModeleSansExtension>.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 `<unk>` 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 |
| `<modele>.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 |