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

20 KiB
Raw Blame History

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 :
    <PackageVersion Include="Microsoft.ML.OnnxRuntime" Version="1.20.1" />
    
  2. src/Omika.App.Local/Omika.App.Local.csproj :
    <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 :
    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.