Premiere commit - Omika
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
using System.Text;
|
||||
using Omika.App.Local.Services;
|
||||
|
||||
namespace Omika.LanguesProbe;
|
||||
|
||||
/// <summary>
|
||||
/// Commandes « inspecter » et « encoder » : tout ce qui se vérifie sur le
|
||||
/// <b>vocabulaire seul</b>, sans moteur d'inférence.
|
||||
///
|
||||
/// <para>
|
||||
/// 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.
|
||||
/// </para>
|
||||
/// </summary>
|
||||
internal static class InspectionVocabulaire
|
||||
{
|
||||
/// <summary>
|
||||
/// 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).
|
||||
/// </summary>
|
||||
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."
|
||||
];
|
||||
|
||||
/// <summary>Commande « inspecter » : identité et comportement du vocabulaire.</summary>
|
||||
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 <unk>/<s>/</s>/<pad> : {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 <unk>, 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;
|
||||
}
|
||||
|
||||
/// <summary>Commande « encoder » : identifiants et pièces, dans l'ordre.</summary>
|
||||
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 <vocab.spm> \"<texte>\" »");
|
||||
}
|
||||
|
||||
SentencePieceTokenizer tokenizer;
|
||||
try
|
||||
{
|
||||
tokenizer = SentencePieceTokenizer.Charger(chemin);
|
||||
}
|
||||
catch (SentencePieceException ex)
|
||||
{
|
||||
return EchecVocabulaire(ex);
|
||||
}
|
||||
|
||||
IReadOnlyList<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>Aller-retour d'une phrase témoin ; vrai si le texte revient identique.</summary>
|
||||
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<int> 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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Texte d'une pièce, tel que le tokeniseur le restitue.
|
||||
///
|
||||
/// <para>
|
||||
/// La table des pièces n'est pas publiée (elle est privée) : la seule lecture
|
||||
/// possible passe par <see cref="SentencePieceTokenizer.Decode"/> sur un
|
||||
/// identifiant isolé. Deux conséquences, rendues visibles à l'affichage :
|
||||
/// </para>
|
||||
/// <list type="bullet">
|
||||
/// <item>le marqueur d'espace ▁ est retraduit en espace puis rogné en bord de
|
||||
/// chaîne : il est retrouvé en décodant <b>deux fois</b> le même identifiant,
|
||||
/// le second exemplaire révélant l'espace de séparation ;</item>
|
||||
/// <item>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 <b>est</b> sa
|
||||
/// valeur.</item>
|
||||
/// </list>
|
||||
/// </summary>
|
||||
private static string TextePiece(SentencePieceTokenizer tokenizer, int id)
|
||||
{
|
||||
if (id == tokenizer.IdTokenInconnu)
|
||||
{
|
||||
return "<unk> — jeton technique";
|
||||
}
|
||||
|
||||
if (id == tokenizer.IdDebut)
|
||||
{
|
||||
return "<s> — jeton technique";
|
||||
}
|
||||
|
||||
if (id == tokenizer.IdFin)
|
||||
{
|
||||
return "</s> — jeton technique";
|
||||
}
|
||||
|
||||
if (id == tokenizer.IdRemplissage)
|
||||
{
|
||||
return "<pad> — 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 "<octet> — fragment d'octet, non affichable isolément";
|
||||
}
|
||||
|
||||
bool porteMarqueur = tokenizer.Decode([id, id]).Length == (2 * seul.Length) + 1;
|
||||
return porteMarqueur ? $"▁{seul}" : seul;
|
||||
}
|
||||
|
||||
/// <summary>Liste d'identifiants, tronquée quand elle dépasse ce qui reste lisible.</summary>
|
||||
private static string AfficherIds(IReadOnlyList<int> 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();
|
||||
|
||||
/// <summary>Identifiant d'un jeton technique, « absent » pour -1 (pièce absente du vocabulaire).</summary>
|
||||
private static string TexteIdentifiant(int id) => id >= 0 ? id.ToString() : "absent";
|
||||
|
||||
/// <summary>
|
||||
/// Le message de <see cref="SentencePieceException"/> est déjà destiné à
|
||||
/// l'utilisateur (français, sans chemin) : il est affiché tel quel.
|
||||
/// </summary>
|
||||
private static int EchecVocabulaire(SentencePieceException ex)
|
||||
{
|
||||
Console.Error.WriteLine($"ÉCHEC : {Program.MessageSansChemin(ex.Message)}");
|
||||
return CodesSortie.OperationEchouee;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user