# Автоматичний переклад файлів відмітки за допомогою EasyNMT

<datetime class="hidden">2024-08-03T13:30</datetime>

<!--category-- EasyNMT, Markdown -->
## Введение

EasyNMT - це локальна інсталяційна служба, яка надає простий інтерфейс багатьом машинам перекладу. В цьому вправі ми використаємо EasyN MT, щоб автоматично перекладати файл Markdown з англійської на кілька мов.

Ви можете знайти всі файли для цього уроку в [Репозиторія GitHub](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid/MarkdownTranslator) для цього проекту.

Результатом цього було створення BUNCH нових файлів відмітки в цільових мовах. Це дуже простий спосіб отримати блоговий пост перекладений на різні мови.

[Переведені листи](/translatedposts.png)

[TOC]

## Потрібні умови

Установка EasyNMT необхідна для того, щоб слідувати за цим інструкцією. Я зазвичай запускаю її як докерну службу . Ви можете знайти інструкції щодо instalacji [тут](https://github.com/UKPLab/EasyNMT/blob/main/docker/README.md) який описує, як запустити його як докер-сервіс.

```shell
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0-cpu
```

Або якщо ви маєте NVIDIA GPU

```shell
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, це простий спосіб перевірити, чи є служба підключена до мережі

```csharp
    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, щоб знайти наступний

```csharp
          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 файлі . Це' проста служба, яка бере строки відмітки та мову-мішанку і повертає перекладений строк відміток

```csharp
    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();
    }
```

Як бачите, він має декілька кроків:

1. `  var document = Markdig.Markdown.Parse(markdown);` - Це розшифровує символ відмітки в документі
2. `  var textStrings = ExtractTextStrings(document);` - Це виокремлює текстові стрічки з документу
   Він використовує метод

```csharp
  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 ' дійсно є роботою.

3. `  var batchSize = 10;` - Це визначає величину пакету для послуг перекладу . EasyNMT має обмеження на кількість слів, які він може перекласти за один раз.
4. `csharp await Post(batch, targetLang, cancellationToken)`
   Це вводить метод, який потім відправляє пакет на службу EasyNMT.

```csharp
    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;
        }
    }
```

5. `  ReinsertTranslatedStrings(document, translatedStrings.ToArray());` - Це повертає переведені строки назад у документ. Використовуючи MarkDig

```csharp

    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 перекладає його на певну кількість мов і записує перетворені файли на диск

```csharp
  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);
                }
            });
        }
       
```

Як бачите, він також перевіряє хеш файлу, щоб побачити, чи він змінився.

Це робиться, вираховуючи швидкий хаш оригінального файлу відмітки, а потім перевіряючи, чи цей файл змінився перед тим, як спробувати його перекласти.

```csharp
    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;
    }
```

Установка в програмі досить проста.

```csharp

    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` щоб витягти перекладені імена для кожного блогового посту.