using System.Text; using Omika.App.Local.Services; namespace Omika.LanguesProbe; /// /// Commandes « inspecter » et « encoder » : tout ce qui se vérifie sur le /// vocabulaire seul, sans moteur d'inférence. /// /// /// C'est la partie utilisable dès aujourd'hui, moteur éteint : un vocabulaire /// illisible, un profil de normalisation approximé, un repli octet absent ou un /// découpage non réversible s'observent ici, alors qu'ils ne se verraient en /// production que sous forme de traductions dégradées. /// /// internal static class InspectionVocabulaire { /// /// Phrases témoins de l'aller-retour : elles cumulent ce qui casse un /// découpage réel — accents, ponctuation espagnole inversée, apostrophes, /// chiffres et unités souvent hors vocabulaire, majuscules en début de /// phrase (le marqueur d'espace y est présent). /// private static readonly string[] PhrasesTemoins = [ "Bonjour, comment allez-vous ?", "Le client a signé le devis hier après-midi.", "Rendez-vous demain matin à 8 h 30.", "L'intervention est terminée : 1 234,56 € de pièces.", "¿Dónde está el cliente?", "La factura se enviará mañana por la mañana.", "Necesito hablar con el responsable de la obra.", "El técnico llegó a las nueve y revisó la caldera." ]; /// Commande « inspecter » : identité et comportement du vocabulaire. public static int Inspecter(string[] args) { Arguments arguments = Arguments.Analyser(args); string chemin = arguments.Positionnel(0, "chemin du vocabulaire (.spm)"); SentencePieceTokenizer tokenizer; try { tokenizer = SentencePieceTokenizer.Charger(chemin); } catch (SentencePieceException ex) { return EchecVocabulaire(ex); } Console.WriteLine("── Vocabulaire ──"); Console.WriteLine($" Fichier : {Program.NomFichier(chemin)}"); Console.WriteLine($" Pièces : {tokenizer.TailleVocabulaire}"); Console.WriteLine($" Jetons /// : {TexteIdentifiant(tokenizer.IdTokenInconnu)} / {TexteIdentifiant(tokenizer.IdDebut)}" + $" / {TexteIdentifiant(tokenizer.IdFin)} / {TexteIdentifiant(tokenizer.IdRemplissage)}"); Console.WriteLine($" Normalisation : {tokenizer.ProfilNormalisation}"); // Le repli octet décide du sort des caractères hors vocabulaire : accents perdus ou // préservés. C'est la seconde chose à vérifier après les jetons techniques. Console.WriteLine(tokenizer.RepliOctetDisponible ? " Repli octet : disponible (un caractère hors vocabulaire est découpé en octets UTF-8)" : " Repli octet : INDISPONIBLE (un caractère hors vocabulaire devient , donc perdu)"); // Le type de segmentation (UNIGRAM / BPE) vit dans le trainer_spec du .spm, que le // tokeniseur lit sans le publier : aucune propriété ne l'expose. L'annoncer demanderait de // le déduire du seul comportement, ce qui serait une supposition — on dit qu'il est // indéterminable ici plutôt que de laisser croire à une mesure. Console.WriteLine(" Segmentation : non déterminable ici (UNIGRAM/BPE non exposé par SentencePieceTokenizer)"); Console.WriteLine(); Console.WriteLine("── Avertissements du tokeniseur ──"); if (tokenizer.Avertissements.Count == 0) { Console.WriteLine(" aucun avertissement"); } else { foreach (string avertissement in tokenizer.Avertissements) { Console.WriteLine($" - {avertissement}"); } } Console.WriteLine(); Console.WriteLine("── Aller-retour : texte → identifiants → texte ──"); Console.WriteLine(" DIFFERENT signale un découpage non réversible : à comparer avec un tokeniseur de référence."); int ecarts = 0; foreach (string phrase in PhrasesTemoins) { if (!AllerRetour(tokenizer, phrase)) { ecarts++; } } Console.WriteLine(); Console.WriteLine(ecarts == 0 ? $"VERDICT : aller-retour IDENTIQUE sur les {PhrasesTemoins.Length} phrases témoins." : $"VERDICT : {ecarts} phrase(s) sur {PhrasesTemoins.Length} ne reviennent pas identiques (premier écart indiqué)."); return CodesSortie.Succes; } /// Commande « encoder » : identifiants et pièces, dans l'ordre. public static int Encoder(string[] args) { Arguments arguments = Arguments.Analyser(args, "texte"); string chemin = arguments.Positionnel(0, "chemin du vocabulaire (.spm)"); // Un texte non cité arrive en plusieurs arguments : les recoller avec des espaces évite de // ne traduire que le premier mot. string texte = arguments.Option("texte") ?? string.Join(' ', arguments.Reste(1)); if (string.IsNullOrEmpty(texte)) { throw new ErreurUsageException("texte manquant : « encoder \"\" »"); } SentencePieceTokenizer tokenizer; try { tokenizer = SentencePieceTokenizer.Charger(chemin); } catch (SentencePieceException ex) { return EchecVocabulaire(ex); } IReadOnlyList ids = tokenizer.EncodeAsync(texte).GetAwaiter().GetResult(); Console.WriteLine($"Vocabulaire : {Program.NomFichier(chemin)} ({tokenizer.TailleVocabulaire} pièces)"); Console.WriteLine($"Texte : {texte}"); Console.WriteLine($"Identifiants ({ids.Count}) : {string.Join(' ', ids)}"); Console.WriteLine(); Console.WriteLine(" # id pièce"); for (int i = 0; i < ids.Count; i++) { Console.WriteLine($" {i,-5} {ids[i],-8} {TextePiece(tokenizer, ids[i])}"); } return CodesSortie.Succes; } /// Aller-retour d'une phrase témoin ; vrai si le texte revient identique. private static bool AllerRetour(SentencePieceTokenizer tokenizer, string phrase) { // L'encodage est synchrone et sans E/S : l'attente directe est légitime dans une // application console, qui n'a pas de contexte de synchronisation à préserver. IReadOnlyList ids = tokenizer.EncodeAsync(phrase).GetAwaiter().GetResult(); string decode = tokenizer.Decode(ids); bool identique = string.Equals(decode, phrase, StringComparison.Ordinal); Console.WriteLine(); Console.WriteLine($" « {phrase} »"); Console.WriteLine($" identifiants ({ids.Count}) : {AfficherIds(ids)}"); Console.WriteLine($" décodé : « {decode} »"); if (identique) { Console.WriteLine(" VERDICT : IDENTIQUE"); } else { // Le premier caractère divergent désigne la pièce fautive : le développeur n'a pas à // comparer les deux chaînes à l'œil. int position = PremiereDivergence(phrase, decode); Console.WriteLine( $" VERDICT : DIFFERENT — premier écart au caractère {position} : " + $"« {CaractereLisible(phrase, position)} » attendu, « {CaractereLisible(decode, position)} » obtenu"); } return identique; } /// /// Texte d'une pièce, tel que le tokeniseur le restitue. /// /// /// La table des pièces n'est pas publiée (elle est privée) : la seule lecture /// possible passe par sur un /// identifiant isolé. Deux conséquences, rendues visibles à l'affichage : /// /// /// le marqueur d'espace ▁ est retraduit en espace puis rogné en bord de /// chaîne : il est retrouvé en décodant deux fois le même identifiant, /// le second exemplaire révélant l'espace de séparation ; /// une pièce d'octet isolée ne forme pas un caractère UTF-8 valide et se /// décode en U+FFFD — c'est en soi l'indice d'un fragment d'octet. Un octet /// ASCII, lui, s'affiche comme le caractère correspondant, qui est sa /// valeur. /// /// private static string TextePiece(SentencePieceTokenizer tokenizer, int id) { if (id == tokenizer.IdTokenInconnu) { return " — jeton technique"; } if (id == tokenizer.IdDebut) { return " — jeton technique"; } if (id == tokenizer.IdFin) { return " — jeton technique"; } if (id == tokenizer.IdRemplissage) { return " — jeton technique"; } string seul = tokenizer.Decode([id]); if (seul.Length == 0) { return "(aucun texte : jeton de contrôle ou identifiant hors vocabulaire)"; } if (seul.All(c => c == '\uFFFD')) { return " — fragment d'octet, non affichable isolément"; } bool porteMarqueur = tokenizer.Decode([id, id]).Length == (2 * seul.Length) + 1; return porteMarqueur ? $"▁{seul}" : seul; } /// Liste d'identifiants, tronquée quand elle dépasse ce qui reste lisible. private static string AfficherIds(IReadOnlyList ids) { const int maximum = 24; var texte = new StringBuilder(); for (int i = 0; i < Math.Min(ids.Count, maximum); i++) { if (i > 0) { texte.Append(' '); } texte.Append(ids[i]); } if (ids.Count > maximum) { texte.Append($" … (+{ids.Count - maximum} identifiants)"); } return texte.ToString(); } private static int PremiereDivergence(string attendu, string obtenu) { int commun = Math.Min(attendu.Length, obtenu.Length); for (int i = 0; i < commun; i++) { if (attendu[i] != obtenu[i]) { return i; } } return commun; } private static string CaractereLisible(string texte, int position) => position >= texte.Length ? "fin de chaîne" : texte[position].ToString(); /// Identifiant d'un jeton technique, « absent » pour -1 (pièce absente du vocabulaire). private static string TexteIdentifiant(int id) => id >= 0 ? id.ToString() : "absent"; /// /// Le message de est déjà destiné à /// l'utilisateur (français, sans chemin) : il est affiché tel quel. /// private static int EchecVocabulaire(SentencePieceException ex) { Console.Error.WriteLine($"ÉCHEC : {Program.MessageSansChemin(ex.Message)}"); return CodesSortie.OperationEchouee; } }