# 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 ``` /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. - `` = 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 ``` 2. `src/Omika.App.Local/Omika.App.Local.csproj` : ```xml $(DefineConstants);OMIKA_ONNX_RUNTIME ``` 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.