using Omika.App.Local.Abstractions; using Omika.App.Local.Models; using Omika.Core.Contracts.Mobile.Languages; namespace Omika.LanguesProbe; /// /// Magasin de pack de langue adossé à un dossier du disque : c'est ce qui permet /// d'exécuter le vrai pipeline (OnnxTranslationService) sur les /// fichiers d'un développeur, hors téléphone. /// /// /// Le dossier passé en ligne de commande est traité comme /// « langues/<code>/<sens> » : tous les fichiers /// *.spm et *.onnx qu'il contient sont déclarés pour le sens /// demandé, avec leur taille réelle. Aucune empreinte SHA-256 n'est inventée : /// un pack décrit depuis le disque n'a pas de référence serveur à laquelle se /// comparer, et une fausse empreinte rendrait la vérification mensongère. /// /// /// /// Lecture seule. Les méthodes d'écriture de l'interface refusent d'agir /// plutôt que de faire semblant : un outil de diagnostic ne doit jamais /// modifier, ni supprimer, ce qu'un développeur lui demande d'examiner. /// /// internal sealed class PackLangueDossier : ILanguagePackStore { private readonly string _dossier; private readonly string _code; private readonly string _sens; public PackLangueDossier(string dossier, string code, string sens) { _dossier = dossier; _code = code; _sens = sens; } /// /// Dossier partagé entre les deux sens d'une même langue /// (« <code>\commun »), frère du dossier examiné. Un modèle familial /// (opus-mt-tc-big-itc-itc) y range ses poids une seule fois, et chaque /// sens ne porte que son modele.json. Absent pour un pack bilingue. /// private string DossierCommun { get { string? parent = Path.GetDirectoryName(_dossier); return string.IsNullOrEmpty(parent) ? string.Empty : Path.Combine(parent, LanguageModelDirections.Commun); } } /// Répertoire racine des packs sur l'appareil — ici, le dossier examiné. public string Racine => _dossier; /// /// Aucun manifeste serveur n'est disponible hors application : le retour /// est l'état « mémoire d'index absente », déjà géré /// par les appelants. /// public Task GetIndexAsync(CancellationToken cancellationToken = default) => Task.FromResult(null); public Task EnregistrerIndexAsync(LanguageManifestResponse manifeste, CancellationToken cancellationToken = default) => throw LectureSeule(); public Task GetPackInstalleAsync(string code, CancellationToken cancellationToken = default) => Task.FromResult(DecrirePack()); public Task> ListerPacksInstallesAsync(CancellationToken cancellationToken = default) { var packs = new List(); if (Directory.Exists(_dossier)) { packs.Add(DecrirePack()); } return Task.FromResult>(packs); } public Task EnregistrerPackAsync(PackLangueInstalle pack, CancellationToken cancellationToken = default) => throw LectureSeule(); public Task SupprimerPackAsync(string code, CancellationToken cancellationToken = default) => throw LectureSeule(); public string PreparerFichierPartiel(string code, string sens, string nomFichier) => throw LectureSeule(); /// /// Décrit ce qui est réellement présent : c'est cette description qui /// décide des fichiers que le service ouvrira, donc ce que l'outil mesure. /// public PackLangueInstalle DecrirePack() { var fichiers = new List(); if (Directory.Exists(_dossier)) { // Tri par nom : deux exécutions successives doivent produire exactement le même // inventaire, sans quoi une comparaison entre deux mesures n'aurait aucun sens. string[] chemins = Directory.GetFiles(_dossier); Array.Sort(chemins, StringComparer.Ordinal); foreach (string chemin in chemins) { string nom = Path.GetFileName(chemin); if (!nom.EndsWith(".spm", StringComparison.OrdinalIgnoreCase) && !nom.EndsWith(".onnx", StringComparison.OrdinalIgnoreCase)) { continue; } fichiers.Add(new FichierModeleInstalle { Sens = _sens, NomFichier = nom, TailleOctets = new FileInfo(chemin).Length }); } } // Poids partagés d'un modèle familial : le service les résout APRÈS le // dossier du sens (voir OnnxTranslationService.FichiersDuSens). Les // omettre ferait déclarer « pack incomplet » un pack familial valide. string commun = DossierCommun; if (commun.Length > 0 && Directory.Exists(commun)) { string[] cheminsCommuns = Directory.GetFiles(commun); Array.Sort(cheminsCommuns, StringComparer.Ordinal); foreach (string chemin in cheminsCommuns) { string nom = Path.GetFileName(chemin); if (!nom.EndsWith(".spm", StringComparison.OrdinalIgnoreCase) && !nom.EndsWith(".onnx", StringComparison.OrdinalIgnoreCase)) { continue; } fichiers.Add(new FichierModeleInstalle { Sens = LanguageModelDirections.Commun, NomFichier = nom, TailleOctets = new FileInfo(chemin).Length }); } } return new PackLangueInstalle { Code = _code, // Le libellé vient du manifeste serveur, absent ici ; il n'est lu par aucun appel du // pipeline de traduction. Libelle = _code, Version = string.Empty, InstalleLeUtc = DateTimeOffset.UtcNow, Fichiers = fichiers }; } public bool FichierPresent(string code, FichierModeleInstalle fichier) => File.Exists(CheminFichier(code, fichier.Sens, fichier.NomFichier)); /// /// Vérifie la présence et la taille relevée à l'inventaire. L'empreinte /// SHA-256 n'est pas recalculée : le pack étant décrit depuis le disque /// lui-même, il n'existe aucune empreinte attendue à confronter. /// public Task VerifierFichierAsync( string code, FichierModeleInstalle fichier, CancellationToken cancellationToken = default) { var info = new FileInfo(CheminFichier(code, fichier.Sens, fichier.NomFichier)); bool conforme = info.Exists && (fichier.TailleOctets <= 0 || info.Length == fichier.TailleOctets); return Task.FromResult(conforme); } /// /// Chemin réel d'un fichier du pack. Le nom est réduit à son nom simple, /// comme dans LanguagePaths : un nom de fichier ne doit jamais /// permettre de sortir du dossier examiné. Un fichier absent du dossier du /// sens est cherché dans son frère commun (poids partagé d'un modèle /// familial), en suivant exactement la priorité du service. /// public string CheminFichier(string code, string sens, string nomFichier) { string nom = Path.GetFileName(nomFichier); string chemin = Path.Combine(_dossier, nom); if (File.Exists(chemin)) { return chemin; } string commun = DossierCommun; if (commun.Length > 0) { string partage = Path.Combine(commun, nom); if (File.Exists(partage)) { return partage; } } return chemin; } private static NotSupportedException LectureSeule() => new("Cet outil de diagnostic ne modifie jamais le dossier examiné."); }