Premiere commit - Omika

This commit is contained in:
2026-10-23 18:46:50 +02:00
commit 8ea76c1b24
2155 changed files with 984301 additions and 0 deletions
+360
View File
@@ -0,0 +1,360 @@
# 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.