361 lines
20 KiB
Markdown
361 lines
20 KiB
Markdown
# 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.
|