Automatische Übersetzung von Markdown-Dateien mit EasyNMT
Einleitung
EasyNMT ist ein lokal installierbarer Dienst, der eine einfache Schnittstelle zu einer Reihe von maschinellen Übersetzungsdiensten bietet In diesem Tutorial werden wir EasyNMM verwenden, um automatisch eine Markdown-Datei aus dem Englischen in mehrere Sprachen zu übersetzen.
Sie finden alle Dateien für dieses Tutorial in der GitHub-Repository für dieses Projekt
Die Ausgabe dieser generierte eine BUNCH von neuen Markdown-Dateien in den Zielsprachen .. . ." Dies ist ein super einfacher Weg, um einen Blog-Post in mehrere Sprachen übersetzt bekommen .
Voraussetzungen
Eine Installation von EasyNMT ist erforderlich, um diesem Tutorial zu folgen. Ich laufe es normalerweise als Docker-Service. Hierher die deckt, wie man es als docker services ausführt.
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0-cpu
ODER wenn Sie eine NVIDIA GPU zur Verfügung haben
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0.2-cuda11.3
Die Umgebungsvariablen MAX..WORKERS-.BACKEND und MAX."."WORLERS".FRONTEND setzen die Anzahl der Arbeiter, mit denen EasyNMT arbeitet.
ANMERKUNG: EasyNMT ist der SMOOTHEST-Dienst, um , zu laufen, aber es ist das beste IM'«ve gefunden für diesen Zweck. Es ist ein wenig persnickety über die Eingabe string it.'s übergeben, so dass Sie müssen möglicherweise einige Pre---Verarbeitung Ihres Eingabetextes tun, bevor Sie es an EasyN MT ?..
Oh und es übersetzte auch zu etwas Unsinn über das Einreichen des Vorschlags zu dem EU-...]betraying es.'.s Trainingssatz
Naive Ansatz zur Belastung Balancing
Easy NMT ist ein Durst Biest, wenn es um Ressourcen geht. ,. Also in meinem MarkdownTranslatorService Ich habe einen super einfachen zufälligen IP-Selektor, der gerade durch die Liste der IPs einer Liste von Maschinen rotiert, die ich benutze, um EasyNMT...
Zunächst macht dies einen Schritt auf der model_name Methode auf dem EasyNMT-Dienst - ,- ist dies eine schnelle Möglichkeit, um zu überprüfen, ob der Dienst up-.-Wenn es ist, fügt es die IP zu einer Liste von funktionierenden IPs -.- Wenn es nicht ,-es doesnts.'-t add it to the list
private string[] IPs = translateServiceConfig.IPs;
public async ValueTask<bool> IsServiceUp(CancellationToken cancellationToken)
{
var workingIPs = new List<string>();
try
{
foreach (var ip in IPs)
{
logger.LogInformation("Checking service status at {IP}", ip);
var response = await client.GetAsync($"{ip}/model_name", cancellationToken);
if (response.IsSuccessStatusCode)
{
workingIPs.Add(ip);
}
}
IPs = workingIPs.ToArray();
if (!IPs.Any()) return false;
return true;
}
catch (Exception e)
{
logger.LogError(e, "Error checking service status");
return false;
}
}
Dann innerhalb der Post Verfahren der MarkdownTranslatorService wir rotieren durch die funktionierenden IPs, um das nächste zu finden.
if(!IPs.Any())
{
logger.LogError("No IPs available for translation");
throw new Exception("No IPs available for translation");
}
var ip = IPs[currentIPIndex];
logger.LogInformation("Sending request to {IP}", ip);
// Update the index for the next request
currentIPIndex = (currentIPIndex + 1) % IPs.Length;
Dies ist eine super einfache Weise, um die Anforderungen über eine Reihe von Maschinen zu laden...Es ist nicht perfekt (.Es doesns.'.t Konto für eine Super-beschäftigte Maschine für exampel.),. Aber es ist gut genug für meine Zwecke.
Der Mistkerl currentIPIndex = (currentIPIndex + 1) % IPs.Length; gerade dreht sich durch die Liste der IPs, beginnend bei 0 und geht auf die Länge der Liste
Eine Markdown-Datei übersetzen
Dies ist der Code, den ich im MarkdownTranslatorService-Datei ..cs habe... Es ist ein einfacher Dienst, der einen Markdown String und eine Zielsprache nimmt und die übersetzte Markdown string... zurückgibt.
public async Task<string> TranslateMarkdown(string markdown, string targetLang, CancellationToken cancellationToken)
{
var document = Markdig.Markdown.Parse(markdown);
var textStrings = ExtractTextStrings(document);
var batchSize = 10;
var stringLength = textStrings.Count;
List<string> translatedStrings = new();
for (int i = 0; i < stringLength; i += batchSize)
{
var batch = textStrings.Skip(i).Take(batchSize).ToArray();
translatedStrings.AddRange(await Post(batch, targetLang, cancellationToken));
}
ReinsertTranslatedStrings(document, translatedStrings.ToArray());
return document.ToMarkdownString();
}
Wie Sie sehen können, hat es eine Reihe von Schritten.
var document = Markdig.Markdown.Parse(markdown);Dieses pariert die Markierungskette in ein Dokument.var textStrings = ExtractTextStrings(document);Dieses extrahiert die Textzeichenfolgen aus dem Dokument. Dabei wird die Methode
private bool IsWord(string text)
{
var imageExtensions = new[] { ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".svg" };
if (imageExtensions.Any(text.Contains)) return false;
return text.Any(char.IsLetter);
}
Dieses überprüft, ob die -'-Wort -' ist wirklich eine Arbeit - ;-Bildnamen können bis der Satz Splitting Funktionalität in EasyNMT .
var batchSize = 10;Dieses setzt die Batch-Größe für den Übersetzungsdienst ein.... EasyNMT hat eine Grenze für die Anzahl der Wörter, die es in einem Schritt übersetzen kann.csharp await Post(batch, targetLang, cancellationToken)Dies ruft in die Methode auf, die dann das Batch an den EasyNMT-Dienst postet.
private async Task<string[]> Post(string[] elements, string targetLang, CancellationToken cancellationToken)
{
try
{
var postObject = new PostRecord(targetLang, elements);
var response = await client.PostAsJsonAsync("/translate", postObject, cancellationToken);
var phrase = response.ReasonPhrase;
response.EnsureSuccessStatusCode();
var result = await response.Content.ReadFromJsonAsync<PostResponse>(cancellationToken: cancellationToken);
return result.translated;
}
catch (Exception e)
{
logger.LogError(e, "Error translating markdown: {Message} for strings {Strings}", e.Message, string.Concat( elements, Environment.NewLine));
throw;
}
}
ReinsertTranslatedStrings(document, translatedStrings.ToArray());Dieses fügt die übersetzten Zeichenfolgen wieder in das Dokument ein.... mit MarkDig-'s Fähigkeit, das Dokument zu gehen und Textzeichenketten zu ersetzen....
private void ReinsertTranslatedStrings(MarkdownDocument document, string[] translatedStrings)
{
int index = 0;
foreach (var node in document.Descendants())
{
if (node is LiteralInline literalInline && index < translatedStrings.Length)
{
var content = literalInline.Content.ToString();
if (!IsWord(content)) continue;
literalInline.Content = new Markdig.Helpers.StringSlice(translatedStrings[index]);
index++;
}
}
}
Gehosteter Dienst
Um all dies auszuführen, benutze ich einen IHostedLifetimeService, der in der Programmdatei gestartet wird.. Dieser Dienst liest sich in einer Markdown-Datei und übersetzt sie in eine Reihe von Sprachen.
public async Task StartedAsync(CancellationToken cancellationToken)
{
if(!await blogService.IsServiceUp(cancellationToken))
{
logger.LogError("Translation service is not available");
return;
}
ParallelOptions parallelOptions = new() { MaxDegreeOfParallelism = blogService.IPCount, CancellationToken = cancellationToken};
var files = Directory.GetFiles(markdownConfig.MarkdownPath, "*.md");
var outDir = markdownConfig.MarkdownTranslatedPath;
var languages = translateServiceConfig.Languages;
foreach(var language in languages)
{
await Parallel.ForEachAsync(files, parallelOptions, async (file,ct) =>
{
var fileChanged = await file.IsFileChanged(outDir);
var outName = Path.GetFileNameWithoutExtension(file);
var outFileName = $"{outDir}/{outName}.{language}.md";
if (File.Exists(outFileName) && !fileChanged)
{
return;
}
var text = await File.ReadAllTextAsync(file, cancellationToken);
try
{
logger.LogInformation("Translating {File} to {Language}", file, language);
var translatedMarkdown = await blogService.TranslateMarkdown(text, language, ct);
await File.WriteAllTextAsync(outFileName, translatedMarkdown, cancellationToken);
}
catch (Exception e)
{
logger.LogError(e, "Error translating {File} to {Language}", file, language);
}
});
}
Wie Sie sehen können, überprüft es auch den Hash der Datei, um zu sehen, ob sie sich vor ihrer Übersetzung geändert hat... Dies ist zu vermeiden, dass Dateien übersetzt werden, die Haven-'.
Dies wird durch die Berechnung eines schnellen Hash der ursprünglichen Markdown-Datei dann Testen, um zu sehen, ob diese Datei sich geändert hat, bevor versucht, es zu übersetzen
private static async Task<string> ComputeHash(string filePath)
{
await using var stream = File.OpenRead(filePath);
stream.Position = 0;
var bytes = new byte[stream.Length];
await stream.ReadAsync(bytes);
stream.Position = 0;
var hash = XxHash64.Hash(bytes);
var hashString = Convert.ToBase64String(hash);
hashString = InvalidCharsRegex.Replace(hashString, "_");
return hashString;
}
Das Setup im Programm ist ziemlich einfach.
builder.Services.AddHostedService<BackgroundTranslateService>();
services.AddHttpClient<MarkdownTranslatorService>(options =>
{
options.Timeout = TimeSpan.FromMinutes(15);
});
Ich richte den HostedService ein und den HttpClient für den MarkdownTranslatorService. Ein Hosted Service ist ein langer, im Hintergrund ausgeführter Dienst, der einen guten Platz bietet, um Dienstleistungen anzubieten, die kontinuierlich im Hintergrund laufen müssen oder nur eine Weile dauern müssen, bis das neue IHostedLifetimeService Interface etwas flexibler als das alte IHostedService Interface ist und uns Aufgaben ganz im Hintergrund leichter ausführen lässt als der ältere IHostedService.
Hier sehen Sie, wie I'-m den Timeout für den HttpClient auf 15-Minuten einstellt.Dies liegt daran, dass EasyNMT ein wenig langsam reagieren kann.Besonders das erste Mal mit einem Sprachmodell.).-I'-m setzt auch die Basisadresse auf die IP-Adresse des Rechners, der den EasyN MT Service betreibt.
Schlussfolgerung
Dies ist eine ziemlich einfache Art und Weise, um eine Markdown-Datei in mehrere Sprachen zu übersetzen - . - Es ' ist nicht perfekt -, - aber es ist ein guter Start .. - Ich führe dies für jeden neuen Blog-Post und es wird in der MarkdownBlogService um die übersetzten Namen für jeden Blog-Post zu ziehen