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;
}