using System.Reflection; using System.Text.RegularExpressions; namespace Omika.LanguesProbe; /// /// Omika.LanguesProbe — vérifie un couple de traduction hors ligne (FR ⇄ ES) /// sur le poste de développement, à partir des fichiers réels du pack. /// /// /// Pourquoi cet outil. Tant que le symbole OMIKA_ONNX_RUNTIME n'est /// pas défini, le moteur d'inférence n'est embarqué dans aucune tête de /// livraison : contrôler un vocab.spm et un *.onnx réels /// (identifiants des jetons techniques, normalisation, aller-retour du /// découpage, traduction complète) supposait un déploiement sur téléphone. Ici /// les fichiers sont lus directement sur le disque : c'est le véhicule /// d'exécution qui permet de décider de l'activation du moteur à partir de /// mesures, et non de suppositions. /// /// /// /// Aucun paquet NuGet supplémentaire : l'outil n'utilise que du code déjà /// présent dans Omika.App.Local, qui compile et s'exécute sans ONNX /// Runtime. /// /// /// /// Codes de sortie : 0 succès, 1 erreur d'usage (argument manquant, /// option ou commande inconnue), 2 opération en échec (fichier absent, /// vocabulaire refusé, traduction échouée). /// /// public static class Program { /// /// Version lue dans les métadonnées d'assembly, elle-même issue de /// Directory.Build.props : un littéral ici dériverait de la version de /// la solution sans que personne ne s'en aperçoive. /// private static readonly string VersionOutil = typeof(Program).Assembly.GetCustomAttribute()?.InformationalVersion ?? typeof(Program).Assembly.GetName().Version?.ToString() ?? "version inconnue"; // Motif des chemins absolus (Windows « C:\a\b » comme Unix « /a/b »), réduits au seul nom de // fichier dans les messages : un diagnostic ne doit pas divulguer l'arborescence de la machine. private static readonly Regex CheminAbsolu = new(@"(?:[A-Za-z]:)?(?:[\\/][^\\/\s""']+){2,}", RegexOptions.Compiled); public static int Main(string[] args) { try { return Executer(args); } catch (AideDemandeeException) { // « --aide » demandé au milieu d'une commande : ce n'est pas une erreur. AfficherAide(); return CodesSortie.Succes; } catch (ErreurUsageException ex) { Console.Error.WriteLine($"ERREUR D'USAGE : {ex.Message}"); Console.Error.WriteLine("Utilisez « --aide » pour la liste des commandes."); return CodesSortie.ErreurUsage; } catch (Exception ex) { // Aucune trace technique ne sort d'ici : le message d'une exception .NET peut contenir // le chemin absolu d'un fichier. Le nom de fichier seul suffit à agir. Console.Error.WriteLine($"ÉCHEC : {MessageSansChemin(ex.Message)}"); return CodesSortie.OperationEchouee; } } /// /// Nom de fichier seul, pour tout message destiné à l'écran. Retourne une /// mention neutre plutôt que le chemin brut quand le chemin n'a pas de nom /// exploitable (chemin racine, chemin terminé par un séparateur). /// internal static string NomFichier(string chemin) { string nom = Path.GetFileName(chemin.TrimEnd('\\', '/')); return string.IsNullOrEmpty(nom) ? "(élément sans nom)" : nom; } /// /// Remplace les chemins absolus d'un message par leur seul nom de fichier. /// Les messages de ce dépôt évitent déjà les chemins, mais ceux du framework /// (IOException, FileNotFoundException) n'en donnent aucune /// garantie : le filtre est donc appliqué à la frontière, une seule fois. /// internal static string MessageSansChemin(string message) => CheminAbsolu.Replace(message, m => Path.GetFileName(m.Value)); /// /// Rappel actionnable quand aucun moteur d'inférence n'est utilisable : sans /// lui, l'échec serait incompréhensible alors que c'est l'état attendu de la /// machine de développement. /// internal static void AfficherMoteurAbsent() { Console.Error.WriteLine(); Console.Error.WriteLine(" La traduction ne peut pas être calculée : le moteur d'inférence n'est pas"); Console.Error.WriteLine(" compilé dans Omika.App.Local (symbole OMIKA_ONNX_RUNTIME absent)."); Console.Error.WriteLine(" → Activer le moteur : docs\\VALIDATION_TRADUCTION_ONNX.md"); Console.Error.WriteLine(" (procédure et état actuels : docs\\TRADUCTION_HORS_LIGNE_MOBILE.md, §7)"); Console.Error.WriteLine(" → « inspecter » et « encoder » restent utilisables sans moteur."); } private static int Executer(string[] args) { PreparerConsole(); AfficherEnTete(); if (args.Length == 0) { // L'aide est la réponse utile, mais l'appel est incomplet : le code de sortie le dit, // pour qu'un script distingue « commande absente » de « commande réussie ». AfficherAide(); return CodesSortie.ErreurUsage; } string commande = args[0].Trim().ToLowerInvariant(); string[] reste = args[1..]; switch (commande) { case "--aide" or "-h" or "aide": AfficherAide(); return CodesSortie.Succes; case "inspecter": return InspectionVocabulaire.Inspecter(reste); case "encoder": return InspectionVocabulaire.Encoder(reste); case "traduire": return TraductionEssai.Traduire(reste); default: Console.Error.WriteLine($"ERREUR D'USAGE : commande inconnue « {args[0]} »."); AfficherAide(); return CodesSortie.ErreurUsage; } } /// /// Sortie en UTF-8 : les messages sont en français et l'affichage des pièces /// utilise le marqueur d'espace ▁ du modèle. Sans cela, une console Windows /// restée sur une page de codes héritée rendrait des caractères de /// remplacement — un diagnostic illisible ne sert à rien. Un échec est /// ignoré : une sortie redirigée n'a pas de console à reconfigurer. /// private static void PreparerConsole() { try { Console.OutputEncoding = System.Text.Encoding.UTF8; } catch (Exception) { // Aucune console interactive : l'encodage demandé par l'appelant fait foi. } } private static void AfficherEnTete() => Console.WriteLine( $"Omika.LanguesProbe {VersionOutil} — validation d'un couple de traduction hors ligne (FR ⇄ ES), " + "sans déploiement." + Environment.NewLine); private static void AfficherAide() { Console.WriteLine( """ COMMANDES inspecter Lit un vocabulaire SentencePiece réel et affiche : nombre de pièces, identifiants des jetons / / / (« absent » vaut -1), profil de normalisation, repli octet, avertissements du tokeniseur, puis un aller-retour texte → identifiants → texte sur des phrases françaises et espagnoles. Ne nécessite AUCUN moteur ONNX. encoder Découpe un texte et affiche chaque identifiant avec la pièce qu'il porte, dans l'ordre. Sert à comparer avec un tokeniseur de référence (sentencepiece, transformers) pour localiser une divergence de découpage. Le texte peut être cité entre guillemets ou donné en plusieurs arguments. traduire --dossier --texte "" [--sens ] [--code ] Décrit le pack RÉELLEMENT présent dans (fichiers *.spm et *.onnx), puis exécute le pipeline complet de OnnxTranslationService : résultat, temps de chargement du vocabulaire, temps de chargement de chaque modèle et durée de la traduction. Le dossier est traité comme « langues/es/ » : ses fichiers sont lus tels quels, rien n'y est écrit ni modifié. --sens versFrancais (défaut) | depuisFrancais --code code de langue du pack (défaut : es) --texte texte à traduire (obligatoire) --aide, -h Affiche cette aide. CODES DE SORTIE 0 succès 1 erreur d'usage (argument manquant, option ou commande inconnue) 2 opération en échec (fichier absent, vocabulaire refusé, traduction échouée) SANS MOTEUR ONNX RUNTIME Le moteur n'est compilé que sous le symbole OMIKA_ONNX_RUNTIME (voir docs\VALIDATION_TRADUCTION_ONNX.md ; état actuel : docs\TRADUCTION_HORS_LIGNE_MOBILE.md, §7). « inspecter » et « encoder » fonctionnent sans lui ; « traduire » explique alors que la traduction est indisponible, sans jamais afficher de trace technique. EXEMPLES Omika.LanguesProbe inspecter "C:\packs\es\versFrancais\vocab.spm" Omika.LanguesProbe encoder "C:\packs\es\versFrancais\vocab.spm" "Bonjour, comment allez-vous ?" Omika.LanguesProbe traduire --dossier "C:\packs\es\versFrancais" --texte "La factura se enviará mañana." """); } } /// Codes de sortie du programme, énoncés aussi par --aide. internal static class CodesSortie { internal const int Succes = 0; internal const int ErreurUsage = 1; internal const int OperationEchouee = 2; }