Files
2026-10-23 18:46:50 +02:00

211 lines
8.1 KiB
C#

using Omika.App.Local.Abstractions;
using Omika.App.Local.Models;
using Omika.App.Local.Services;
using Omika.Core.Contracts.Mobile.Languages;
namespace Omika.Verif.Langues;
/// <summary>
/// Vérification de la traduction hors ligne sur les VRAIS modèles du catalogue.
///
/// <para>
/// Ce petit exécutable joue le rôle du téléphone : il charge les packs depuis un
/// répertoire local et traduit quelques phrases connues dans les deux sens. Il
/// sert à prouver, avant de livrer un APK, que le pipeline ONNX produit bien du
/// texte — un test unitaire ne peut pas le faire sans embarquer 250 Mo de poids.
/// </para>
///
/// <para>
/// Usage : <c>Omika.Verif.Langues.exe [racine_packs]</c>
/// (par défaut <c>D:\Avalonia\Omika\packs</c>).
/// </para>
/// </summary>
public static class Program
{
private static readonly (string Code, SensTraduction Sens, string Texte, string Attendu)[] Essais =
[
("es", SensTraduction.VersFrancais, "el techo estaba mojado", "Le plafond était mouillé."),
("en", SensTraduction.VersFrancais, "The ceiling was wet.", "Le plafond était mouillé."),
("de", SensTraduction.VersFrancais, "Die Decke war nass.", ""),
("pt", SensTraduction.VersFrancais, "O telhado estava molhado.", "Le toit était humide."),
("it", SensTraduction.VersFrancais, "Il soffitto era bagnato.", "Le plafond était humide."),
("es", SensTraduction.DepuisFrancais, "Le plafond était mouillé.", "El techo estaba mojado."),
("pt", SensTraduction.DepuisFrancais, "Le plafond était mouillé.", "O teto estava molhado."),
];
public static async Task<int> Main(string[] args)
{
string racine = args.Length > 0 ? args[0] : @"D:\Avalonia\Omika\packs";
var depot = new DepotDisque(racine);
using var service = new OnnxTranslationService(depot, OnnxRuntimeInferenceSessionFactory.Instance);
Console.WriteLine($"Moteur : {OnnxRuntimeInferenceSessionFactory.Instance.NomMoteur}");
Console.WriteLine($"Packs : {racine}");
Console.WriteLine();
int echecs = 0;
foreach ((string code, SensTraduction sens, string texte, string attendu) in Essais)
{
var montre = System.Diagnostics.Stopwatch.StartNew();
try
{
string resultat = await service.TraduireAsync(texte, code, sens);
montre.Stop();
bool boucle = DetecterBoucle(resultat);
bool conforme = attendu.Length == 0
|| string.Equals(resultat.Trim(), attendu, StringComparison.OrdinalIgnoreCase);
if (boucle || !conforme)
{
echecs++;
}
string verdict = boucle ? "BOUCLE" : conforme ? "OK" : "ECART";
Console.WriteLine($"[{verdict,-6}] {code} {sens,-14} {montre.ElapsedMilliseconds,6} ms « {texte} » -> « {resultat} »");
if (!conforme && attendu.Length > 0)
{
Console.WriteLine($" attendu : « {attendu} »");
}
}
catch (Exception ex)
{
echecs++;
montre.Stop();
Console.WriteLine($"[ERREUR] {code} {sens} : {ex.GetType().Name} — {ex.Message}");
}
}
Console.WriteLine();
Console.WriteLine("--- Deuxième passe (modèles déjà chargés : latence réelle perçue) ---");
foreach ((string code, SensTraduction sens, string texte, _) in Essais)
{
var montre = System.Diagnostics.Stopwatch.StartNew();
string resultat = await service.TraduireAsync(texte, code, sens);
montre.Stop();
Console.WriteLine($" {code} {sens,-14} {montre.ElapsedMilliseconds,6} ms « {resultat} »");
}
Console.WriteLine();
Console.WriteLine(echecs == 0
? "RÉSULTAT : tous les essais sont conformes."
: $"RÉSULTAT : {echecs} essai(s) en échec.");
return echecs == 0 ? 0 : 1;
}
/// <summary>Vrai si le texte contient un même mot répété 5 fois d'affilée.</summary>
private static bool DetecterBoucle(string texte)
{
string[] mots = texte.Split(' ', StringSplitOptions.RemoveEmptyEntries);
int suite = 1;
for (int i = 1; i < mots.Length; i++)
{
suite = string.Equals(mots[i], mots[i - 1], StringComparison.OrdinalIgnoreCase) ? suite + 1 : 1;
if (suite >= 5)
{
return true;
}
}
return false;
}
}
/// <summary>
/// Dépôt en lecture seule sur un répertoire de packs : c'est exactement la
/// disposition produite par <c>scripts\Exporter-Packs-Langues.ps1</c>.
/// </summary>
internal sealed class DepotDisque(string racine) : ILanguagePackStore
{
private static readonly string[] SensPossibles =
[
LanguageModelDirections.VersFrancais,
LanguageModelDirections.DepuisFrancais,
LanguageModelDirections.Commun
];
public string Racine { get; } = racine;
public string CheminFichier(string code, string sens, string nomFichier)
=> Path.Combine(Racine, code, sens, nomFichier);
public Task<PackLangueInstalle?> GetPackInstalleAsync(
string code, CancellationToken cancellationToken = default)
{
string dossier = Path.Combine(Racine, code);
if (!Directory.Exists(dossier))
{
return Task.FromResult<PackLangueInstalle?>(null);
}
var fichiers = new List<FichierModeleInstalle>();
foreach (string sens in SensPossibles)
{
string cheminSens = Path.Combine(dossier, sens);
if (!Directory.Exists(cheminSens))
{
continue;
}
foreach (string fichier in Directory.EnumerateFiles(cheminSens))
{
fichiers.Add(new FichierModeleInstalle
{
Sens = sens,
NomFichier = Path.GetFileName(fichier),
TailleOctets = new FileInfo(fichier).Length,
Sha256 = string.Empty
});
}
}
return Task.FromResult<PackLangueInstalle?>(new PackLangueInstalle
{
Code = code,
Libelle = code,
Version = "1.0.0",
InstalleLeUtc = DateTimeOffset.UtcNow,
Fichiers = fichiers
});
}
public Task<IReadOnlyList<PackLangueInstalle>> ListerPacksInstallesAsync(
CancellationToken cancellationToken = default)
{
var packs = new List<PackLangueInstalle>();
foreach (string dossier in Directory.EnumerateDirectories(Racine))
{
PackLangueInstalle? pack = GetPackInstalleAsync(Path.GetFileName(dossier), cancellationToken)
.GetAwaiter().GetResult();
if (pack is not null)
{
packs.Add(pack);
}
}
return Task.FromResult<IReadOnlyList<PackLangueInstalle>>(packs);
}
public Task<LanguageManifestResponse?> GetIndexAsync(CancellationToken cancellationToken = default)
=> Task.FromResult<LanguageManifestResponse?>(null);
public Task EnregistrerIndexAsync(LanguageManifestResponse manifeste, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task EnregistrerPackAsync(PackLangueInstalle pack, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public Task SupprimerPackAsync(string code, CancellationToken cancellationToken = default)
=> Task.CompletedTask;
public bool FichierPresent(string code, FichierModeleInstalle fichier)
=> File.Exists(CheminFichier(code, fichier.Sens, fichier.NomFichier));
public Task<bool> VerifierFichierAsync(
string code, FichierModeleInstalle fichier, CancellationToken cancellationToken = default)
=> Task.FromResult(FichierPresent(code, fichier));
public string PreparerFichierPartiel(string code, string sens, string nomFichier)
=> CheminFichier(code, sens, nomFichier + ".partiel");
}