Files
Omika/docs/TRADUCTION_HORS_LIGNE_MOBILE.md
2026-10-23 18:46:50 +02:00

361 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Traduction hors ligne — application mobile Omika
> **Mise en place côté mobile** de la traduction hors ligne : langue secondaire,
> téléchargement des modèles, dictée traduite et bouton « Traduire ».
> Le service serveur qui distribue les modèles est décrit dans
> `DEPLOIEMENT_API_LANGUES.md` (à lire **avant** ce document).
> **Cible** : `Omika.App` (code partagé), têtes `Omika.App.Android` /
> `Omika.App.iOS`, socle local `Omika.App.Local`.
---
## 1. Règles absolues (non négociables)
1. **La langue métier reste le français.** Tout ce qui est enregistré, mis dans
l'Outbox, synchronisé ou stocké dans HFSQL est du **français**. Une dictée en
langue secondaire est traduite **avant** d'entrer dans le champ : le texte
d'origine n'est jamais conservé.
2. **Le texte ne sort jamais de l'appareil.** Aucun appel réseau n'est fait pour
traduire : l'API des langues ne sert qu'à **descendre** des modèles.
3. **La traduction de lecture est un affichage seul.** Le texte traduit d'un
contenu (note, description) n'est écrit ni dans SQLite, ni dans l'Outbox, ni
vers l'API : il disparaît dès que le contenu change ou que l'écran est quitté.
4. **Aucune préférence de langue n'est envoyée au serveur.** La langue secondaire
est une préférence **locale par utilisateur**, au même titre que la pause
déjeuner (table technique `PreferencesUtilisateur`).
---
## 2. Flux d'ensemble
```
┌──────────────────────────────────────────┐
HFSQL │ ParametreSysteme PAS_Code = SRV_LANGUES │
(serveur) └───────────────────┬──────────────────────┘
│ bootstrap
▼
┌──────────────────────────────────────────┐
Omika.Api │ GET /api/mobile/bootstrap │
│ → MobileConfigurationDto.LanguesUrl │
└───────────────────┬──────────────────────┘
│
▼
┌──────────────────────────────────────────┐
Omika.Langues.Api│ GET /api/langues (manifeste) │
(anonyme) │ GET /api/langues/{code}/{sens}/{fichier} │
└───────────────────┬──────────────────────┘
│ téléchargement + SHA-256
▼
Appareil ┌──────────────────────────────────────────┐
│ Omika/langues/{code}/pack.json + fichiers │
└───────────────────┬──────────────────────┘
│
┌───────────────────┴──────────────────────┐
▼ ▼
Dictée traduite (es → fr) Bouton « Traduire » (fr → es)
champ de saisie contenu en lecture seule
```
---
## 3. Fichiers créés ou modifiés
### 3.1 Socle local — `src/Omika.App.Local`
| Fichier | Rôle |
|---|---|
| `Models/LangueSecondairePreference.cs` | Préférence de langue secondaire (code ISO + libellé) |
| `Models/SensTraduction.cs` | Sens de traduction (`versFrancais` / `depuisFrancais`) |
| `Models/LangueDisponible.cs` | Langue proposée + état d'installation ; `PackLangueInstalle` (`pack.json`) |
| `Abstractions/ILangueSecondairePreferenceService.cs` | Lecture/écriture de la préférence |
| `Services/LangueSecondairePreferenceService.cs` | Implémentation (SQLite, par utilisateur) |
| `Abstractions/ILanguagePackService.cs` | Manifeste, téléchargement, suppression, `ProgressionTelechargement` |
| `Abstractions/ILanguagePackStore.cs` | Dépôt local (marqueurs, empreintes, chemins) |
| `Services/LanguagePackService.cs` | Téléchargement + vérification SHA-256 + marquage |
| `Services/LanguagePackStore.cs` | Écriture/lecture disque, calcul SHA-256 |
| `Infrastructure/LanguagePaths.cs` | Arborescence locale + assainissement des codes/noms |
| `Infrastructure/LanguageTag.cs` | Code ISO (`es`) → culture BCP-47 (`es-ES`) pour la dictée |
| `Abstractions/ITranslationService.cs` | Traduction hors ligne + `TranslationException` |
| `Abstractions/IOnnxInferenceSessionFactory.cs` | **Seam** avec ONNX Runtime |
| `Abstractions/OnnxTensor.cs` | Tenseur minimal (entiers / réels) |
| `Abstractions/ITextTokenizer.cs` | Tokenisation (`EncodeAsync` / `Decode`) |
| `Services/SentencePieceTokenizer.cs` | Tokeniseur SentencePiece (UNIGRAM/BPE), lecture protobuf autonome |
| `Services/OnnxTranslationService.cs` | Pipeline complet + `OnnxTranslationOptions` |
| `Services/UnavailableOnnxInferenceSessionFactory.cs` | Repli : traduction « indisponible » |
| `Services/OnnxRuntimeInferenceSessionFactory.cs` | **Moteur réel — compilé seulement sous `OMIKA_ONNX_RUNTIME`** |
| `Abstractions/IDicteeTraduiteService.cs` | Orchestration de la dictée traduite + `DicteeTraduiteService` |
Fichiers modifiés : `Sqlite/SqliteSchema.cs` (2 colonnes + 2 migrations),
`Repositories/PreferencesUtilisateurRepository.cs`,
`Abstractions/IPreferencesUtilisateurRepository.cs`,
`Abstractions/IDictationService.cs` (**langue de dictée** + messages),
`Infrastructure/` (inchangé par ailleurs).
### 3.2 Application — `src/Omika.App`
| Fichier | Rôle |
|---|---|
| `Services/PlatformDicteeTraduite.cs` | Injection des contrôles + cache d'état (invalidable) |
| `ViewModels/LangueOptionViewModel.cs` | Ligne de langue (état, progression, actions) |
| `ViewModels/OnboardingViewModel.cs` | Écran d'accueil de première inscription |
| `Views/OnboardingView.axaml(.cs)` | Vue de l'accueil (3 étapes) |
| `Controls/TranslateButton.axaml(.cs)` | Bouton « Traduire » des contenus en lecture seule |
Fichiers modifiés : `AppServices.cs` (composition root), `Navigation/PageViewModelFactory.cs`,
`ViewModels/ParametresViewModel.cs`, `Views/ParametresView.axaml`,
`ViewModels/RootViewModel.cs` (étape d'accueil),
`Controls/DictationTextField.axaml(.cs)` (panneau de dictée + bascule),
`Views/InterventionNotesView.axaml`, `Views/InterventionNotesAdministrativesView.axaml`,
`Views/InterventionDetailView.axaml`, `Views/RendezVousDetailView.axaml`.
### 3.3 Têtes plateformes
| Fichier | Modification |
|---|---|
| `src/Omika.App.Android/AndroidDictationService.cs` | `StartAsync(codeLangue)` → `LanguageTag.DepuisCode` |
| `src/Omika.App.iOS/IosDictationService.cs` | idem (`NSLocale.FromLocaleIdentifier`) |
| `src/Omika.App/Services/UnavailableDictationService.cs` | signature alignée |
---
## 4. Stockage local des modèles
```
<données application>/Omika/langues/
├── index.json dernier manifeste serveur connu (liste hors ligne)
├── es/
│ ├── pack.json pack INSTALLÉ (version + empreintes)
│ ├── versFrancais/ es -> fr
│ │ ├── encoder_model.onnx
│ │ ├── decoder_model.onnx
│ │ ├── source.spm vocabulaire ESPAGNOL
│ │ └── target.spm vocabulaire FRANÇAIS
│ └── depuisFrancais/ fr -> es
│ ├── encoder_model.onnx
│ ├── decoder_model.onnx
│ ├── source.spm vocabulaire FRANÇAIS
│ └── target.spm vocabulaire ESPAGNOL
└── pt/ ...
```
> **Deux vocabulaires par sens.** Un modèle Marian/OPUS-MT a un vocabulaire par
> langue : `source.spm` (entrée) et `target.spm` (sortie). Les livrer **tous les
> deux** est obligatoire — décoder la sortie avec le vocabulaire d'entrée produit
> des identifiants interprétés dans le mauvais référentiel, donc du texte sans
> aucun sens. Un modèle à vocabulaire **partagé** (NLLB, mBART, M2M-100) n'a
> besoin que d'un `vocab.spm`, qui sert alors aux deux sens. Le service résout
> ces noms seul, sans configuration.
>
> Ne **jamais** livrer `decoder_with_past_model.onnx` (décodeur à cache
> `past_key_values`) : il est ignoré par le service.
- `<données application>` = sandbox interne Android / dossier `Library` iOS
(voir `MobileStoragePaths.DataDirectory`).
- **`pack.json` est écrit en DERNIER**, après vérification de **tous** les
fichiers : sa présence est la preuve que le pack est complet et intègre.
- Un téléchargement interrompu laisse des fichiers `.part` — jamais promus — et
**aucun** `pack.json` : le pack n'est pas « installé à moitié ».
- Un retéléchargement **ignore** les fichiers déjà présents et conformes
(empreinte recalculée), donc reprendre après une coupure ne reconsomme pas de
données mobiles.
---
## 5. Intégrité et sécurité du téléchargement
1. **Assainissement.** Le code de langue est validé
(`LanguagePaths.EstCodeValide` : minuscules ASCII, chiffres, tiret) et le nom
de fichier est réduit à son nom simple (`Path.GetFileName`) : un manifeste
serveur ne peut jamais faire écrire hors du dossier de la langue.
2. **Empreinte obligatoire.** Un fichier du manifeste sans `sha256` fait
**refuser tout le pack** : mieux vaut ne rien installer que d'installer un
modèle non vérifiable. L'empreinte est **recalculée** sur les octets reçus —
jamais déduite de la taille ni d'un en-tête HTTP.
3. **Renommage atomique.** Le fichier n'apparaît à sa place définitive
(`File.Move` dans le même dossier) qu'après vérification réussie. Un fichier
non conforme est **supprimé**.
4. **Délais.** Aucun délai global sur le client HTTP (un modèle de 120 Mo sur
réseau mobile dépasse largement les 100 s par défaut). La robustesse vient
d'un **chien de garde d'inactivité** de 60 s, réarmé à chaque octet reçu :
une connexion lente mais qui progresse n'est jamais interrompue.
5. **API anonyme.** Aucun jeton n'est envoyé à l'API des langues.
---
## 6. Base de données locale
Deux colonnes ajoutées à la table technique `PreferencesUtilisateur` :
| Colonne | Type | Rôle |
|---|---|---|
| `LangueSecondaire` | `TEXT NULL` | Code ISO de la langue secondaire (`es`, `pt`…), `NULL` = aucune |
| `LangueSecondaireLibelle` | `TEXT NULL` | Libellé affichable, conservé pour l'écran Paramètres **hors ligne** |
- Ajout **idempotent** via `SqliteSchema.ColumnMigrations` : une base déjà
déployée reçoit les colonnes **sans perdre** les horaires de pause déjeuner.
- « Aucune langue secondaire » est représenté par `NULL`, **jamais** par une
chaîne vide (qui serait relue comme un code de langue valide).
---
## 7. Activation du moteur d'inférence ONNX Runtime
> **État actuel : le moteur n'est PAS embarqué.** L'application compile, se
> déploie, télécharge et vérifie les modèles ; la traduction est annoncée comme
> **indisponible** (`UnavailableOnnxInferenceSessionFactory`) au lieu d'échouer
> silencieusement.
**Pourquoi** : le paquet `Microsoft.ML.OnnxRuntime` embarque une bibliothèque
native par architecture (Android arm64/x86_64, iOS, win-x64) et pèse plusieurs
dizaines de Mo par tête. L'activer est une décision de **poids de livrable**, pas
une décision de code : c'est pourquoi le point de contact est isolé dans un seul
fichier.
**Activation — 3 modifications, aucune ligne de code à écrire :**
1. `Directory.Packages.props` :
```xml
<PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
```
2. `src/Omika.App.Local/Omika.App.Local.csproj` :
```xml
<ItemGroup>
<PackageReference Include="Microsoft.ML.OnnxRuntime" />
</ItemGroup>
<PropertyGroup>
<DefineConstants>$(DefineConstants);OMIKA_ONNX_RUNTIME</DefineConstants>
</PropertyGroup>
```
3. `src/Omika.App/AppServices.cs` — remplacer la fabrique par défaut :
```csharp
IOnnxInferenceSessionFactory onnxFactory = OnnxRuntimeInferenceSessionFactory.Instance;
```
Publier ensuite chaque tête avec le bon identifiant de runtime
(`-r android-arm64`, `-r ios-arm64`…) et **mesurer le poids** de l'APK/IPA.
---
## 8. Ordre de réalisation (ce qui a été fait)
| # | Étape | État |
|---|---|---|
| 1 | Préférence langue secondaire : modèle + repository + migration + écran Paramètres | ✅ |
| 2 | Pipeline ONNX minimal `OnnxTranslationService` | ✅ (moteur à activer §7) |
| 3 | Téléchargement des modèles + intégrité SHA-256 | ✅ |
| 4 | Dictée dynamique (Android/iOS) + panneau de dictée avec bascule | ✅ |
| 5 | Bouton « Traduire » + équipement des vues de contenu | ✅ |
| 6 | Écran d'accueil de première inscription | ✅ |
---
## 9. Comportement à l'écran
### 9.1 Écran Paramètres — section « Langue secondaire »
- Liste des langues du manifeste (mémoire locale d'abord : consultable **hors
connexion**), avec poids, état (« Installée (v1.0.0) », « Non installée —
122 Mo ») et mise à jour disponible.
- **Télécharger** (avec progression en Mo), **Choisir**, **Supprimer**,
**Actualiser la liste**, **Annuler le téléchargement**, **Retirer la langue**.
- Si la langue choisie n'est pas installée, un encart le dit explicitement : la
dictée traduite et le bouton « Traduire » ne peuvent pas fonctionner.
- Sans moteur d'inférence (Desktop), la section reste lisible mais explique que
la traduction est indisponible.
### 9.2 Dictée — `DictationTextField`
- **Sans langue secondaire exploitable** : comportement historique inchangé
(dictée française, insertion directe, message sous le champ).
- **Avec langue secondaire exploitable** : un **panneau de dictée** s'ouvre au
démarrage de l'écoute. Il affiche le texte reconnu en direct, la langue en
cours, et propose la **bascule `⇄`** entre français et langue secondaire (la
bascule **redémarre** l'écoute : un moteur natif ne change pas de langue en
cours de session).
- En dictée dans la langue secondaire, seul le **français traduit** entre dans le
champ ; le panneau montre « Entendu (Espagnol) » puis le résultat.
- **Si la traduction échoue**, le texte reconnu n'est pas perdu : le panneau
reste ouvert et propose « Insérer tel quel ».
- La dernière langue de dictée utilisée est mémorisée pour la session (confort de
saisie, jamais persistée).
### 9.3 Bouton « Traduire » — `TranslateButton`
- **Invisible** si l'utilisateur n'a pas de langue secondaire exploitable, ou si
le contenu est vide.
- Bascule « ⇄ Traduire » / « Voir en français ».
- Le résultat s'affiche sous le bouton, précédé de la mention
« Traduit automatiquement en Espagnol (hors ligne — affichage seul) ».
- Vues équipées : notes internes, notes administratives, description
d'intervention, description de rendez-vous.
### 9.4 Écran d'accueil de première inscription — `OnboardingViewModel`
Affiché **une seule fois** : après l'inscription réussie (QR → licence → API) et
après l'application des droits, **avant** le premier affichage du Shell.
| Étape | Contenu |
|---|---|
| 1 | Langue secondaire : choisir et/ou télécharger les modèles |
| 2 | Thème (clair / sombre / système) + couleur d'accent — aperçu immédiat |
| 3 | Pause déjeuner habituelle (début / fin) |
- Boutons **Retour**, **Suivant**, **Terminer**, **Passer**.
- **Rien n'est obligatoire** : « Passer » ferme l'accueil immédiatement.
- Un appareil **déjà inscrit** qui redémarre va directement au Shell (l'accueil
n'est pas rejoué) ; une **réinscription** le rejoue, ce qui est le
comportement voulu (nouvelle licence, nouveau client).
- La clôture de l'accueil rend la main au Shell : `RootViewModel.ShowOnboarding`
→ `Onboarding.Termine` → `OnActivationSucceeded`.
---
## 10. Diagnostic des problèmes courants
| Symptôme | Cause probable | Action |
|---|---|---|
| Aucune langue dans la liste | `SRV_LANGUES` absent du bootstrap, ou `manifest.json` absent/vide | Vérifier §4 et §5 de `DEPLOIEMENT_API_LANGUES.md`, puis **Actualiser la liste** |
| « Le service des langues ne répond pas » | Service `OmikaLanguesApi` arrêté, port/URL erroné, HTTPS non exposé | §7.8 et §8 du doc de déploiement |
| « empreinte SHA-256 non conforme » | Fichier modifié sur le disque serveur sans régénérer `manifest.json` | Régénérer les empreintes (§5.3 du doc de déploiement) |
| La bascule `⇄` n'apparaît pas | Langue non installée, ou moteur d'inférence absent | Installer la langue dans les Paramètres ; activer le moteur (§7) |
| « Traduction hors ligne indisponible » | Moteur non embarqué (§7) | Activer `OMIKA_ONNX_RUNTIME` |
| Bucket de dictée vide / « Langue de dictée indisponible » | Langue non installée dans le moteur vocal du téléphone | Installer le paquet de langue dans les réglages Android/iOS |
---
## 11. Limites connues et points d'extension
> Les deux premiers points sont traités pas à pas dans
> **`VALIDATION_TRADUCTION_ONNX.md`** : activation du moteur, export d'une vraie
> paire FR⇄ES, validation sur le poste, et mesure de la divergence du tokeniseur.
1. **Moteur ONNX non embarqué** — voir §7. C'est la seule fonctionnalité qui
n'est pas active de bout en bout.
2. **SentencePiece approximé sur un point précis** — `precompiled_charsmap` (la
table de normalisation Unicode compilée du modèle) n'est **pas** appliqué : la
réimplémenter demanderait un trie à double tableau pour un gain nul sur du
texte déjà en NFC, c'est-à-dire tout ce que produit un clavier mobile.
En revanche `add_dummy_prefix`, `escape_whitespaces`,
`remove_extra_whitespaces` et le **repli octet** sont désormais lus et
honorés : le tokeniseur se signale lui-même (`ProfilNormalisation`,
`Avertissements`) au lieu de diverger en silence. Les types `WORD` et `CHAR`
restent refusés au chargement.
3. **Décodage glouton** (pas de *beam search*) : compromis assumé pour la latence
sur téléphone. Le remplacer ne touche que `OnnxTranslationService`.
4. **Deux formes de modèles acceptées** : encodeur + décodeur séparés (un
`decoder*.onnx` hors `*_with_past*` est présent), ou modèle unique produisant
directement la séquence traduite. Les noms d'entrées/sorties sont résolus
**automatiquement** sur le modèle chargé, et restent surchargeables via
`OnnxTranslationOptions`.
5. **Poids des packs** : ~80 à 160 Mo par langue pour un modèle à vocabulaire
partagé, **~300 Mo** pour un Marian/OPUS-MT (deux sens × encodeur + décodeur +
deux vocabulaires). La taille est affichée **avant** téléchargement.
6. **Tests** : le calcul SHA-256, l'assainissement des chemins, le verrou
d'installation et la machine à états de la dictée sont couverts par
`LanguagePackTests` et `DictationTextFieldTests`. Le tokeniseur est couvert par
`SentencePieceFixturesTests`, **contre la référence Python** — voir
`VALIDATION_TRADUCTION_ONNX.md` §3.3. Dans l'environnement de compilation
utilisé ici, ces tests **compilent mais ne s'exécutent pas** (le testhost
VSTest échoue) : ils sont à lancer sur le poste de développement.