286 lines
11 KiB
C#
286 lines
11 KiB
C#
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;
|
|
}
|
|
}
|