Автоматичний переклад файлів відмітки за допомогою EasyNMT
Введение
EasyNMT - це локальна інсталяційна служба, яка надає простий інтерфейс багатьом машинам перекладу. В цьому вправі ми використаємо EasyN MT, щоб автоматично перекладати файл Markdown з англійської на кілька мов.
Ви можете знайти всі файли для цього уроку в Репозиторія GitHub для цього проекту.
Результатом цього було створення BUNCH нових файлів відмітки в цільових мовах. Це дуже простий спосіб отримати блоговий пост перекладений на різні мови.
Потрібні умови
Установка EasyNMT необхідна для того, щоб слідувати за цим інструкцією. Я зазвичай запускаю її як докерну службу . Ви можете знайти інструкції щодо instalacji тут який описує, як запустити його як докер-сервіс.
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0-cpu
Або якщо ви маєте NVIDIA GPU
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0.2-cuda11.3
В середовищі MAX_WORKERSM SK1BACKEND і MAXМSK2WOSKERS _FRONTEND встановлюють кількість працівників, які використовуватиме EasyNMT.
Зверніть увагу, що EasyNMT не є SMOOTHEST-сервісом для запуску, але він найкращий, який я знайшов для цього.
І він також перетворив МСК0 , Конkluzія, "'" на дещо безглуздо про подачу wniosku до ЕС .
Наївний підхід до балансування
Easy NMT - це жахлива bestie, коли йдеться про ресурси, тож у моєму MarkdownTranslatorService я маю дуже простий випадковий IP відбірник, який просто обертається через список IP з списку машин, які я використовую для запуску EasyNMT.
Спочатку це відбувається на model_name метод на сайті EasyNMT, це простий спосіб перевірити, чи є служба підключена до мережі
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;
}
}
Тоді всередині Post метод MarkdownTranslatorService ми обертаємось через робочі IP, щоб знайти наступний
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;
Це дуже простий спосіб завантаження баланса запитів на різних машинах. МСК0. Він не ідеальний. МSK2 не означає, що МСК3 буде надто зайнятою машиною для тестування. М СК4, але він достатньо добрий для моїх цілей
Шмік currentIPIndex = (currentIPIndex + 1) % IPs.Length; просто обертається по списку IP, починаючи з 0 і рухаючись до довжини спискуM SK1
Перекладання файла Markdown
Це код, який я маю в MarkdownTranslatorService.cs файлі . Це' проста служба, яка бере строки відмітки та мову-мішанку і повертає перекладений строк відміток
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();
}
Як бачите, він має декілька кроків:
var document = Markdig.Markdown.Parse(markdown);- Це розшифровує символ відмітки в документіvar textStrings = ExtractTextStrings(document);- Це виокремлює текстові стрічки з документу Він використовує метод
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);
}
Це перевіряє, чи слово ' word ' дійсно є роботою.
var batchSize = 10;- Це визначає величину пакету для послуг перекладу . EasyNMT має обмеження на кількість слів, які він може перекласти за один раз.csharp await Post(batch, targetLang, cancellationToken)Це вводить метод, який потім відправляє пакет на службу EasyNMT.
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());- Це повертає переведені строки назад у документ. Використовуючи MarkDig
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++;
}
}
}
Обслуговлена послуга
Для всього цього я використовую IHostedLifetimeService, що запускається в програмі.cs file. Ця служба читає в файлі відміткиM SK2 перекладає його на певну кількість мов і записує перетворені файли на диск
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);
}
});
}
Як бачите, він також перевіряє хеш файлу, щоб побачити, чи він змінився.
Це робиться, вираховуючи швидкий хаш оригінального файлу відмітки, а потім перевіряючи, чи цей файл змінився перед тим, як спробувати його перекласти.
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;
}
Установка в програмі досить проста.
builder.Services.AddHostedService<BackgroundTranslateService>();
services.AddHttpClient<MarkdownTranslatorService>(options =>
{
options.Timeout = TimeSpan.FromMinutes(15);
});
Я створив хостельну службу (BackgroundTranslateService) і HttpClient для MarkdownTranslatorService . Хостинг Сервіс - це довгий-сервіс, що працює в фоні. Це ' хороше місце для того, щоб помістити послуги, які мусять постійно працювати у фоні або просто зайняти деякий час, щоб закінчитиM SK3 Нова інтерфейс IHostedLifetimeService є трохи більш гнучким ніж стараlний інтерфейс iHostedService і дозволяє нам виконувати завдання повністю в фону з легкістю, ніж стараls IHosedService
Тут ви бачите, як я встановлюю тайм-аут для HttpClient на 15 хвилини . Це тому, що EasyNMT може реагувати трохи повільно МSK3 особливо вперше, використовуючи мову-модель
На завершення
Це досить простий спосіб перекладати файл відмітки на різні мови. MarkdownBlogService щоб витягти перекладені імена для кожного блогового посту.