# Automatisch vertalen van Markdown-bestanden met EasyNMT

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

<!--category-- EasyNMT, Markdown -->
## Inleiding

EasyNMT is een lokaal installeerbaar service dat een eenvoudige interface biedt voor een aantal machine-overdrachtsdiensten.. In deze tutorial, gebruiken we EasyN MT om automatisch een Markdown-file van Engels naar meerdere talen te vertalen.

Je kunt alle lêers vinden voor dit tutorial in de [GitHub-repository](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid/MarkdownTranslator) voor dit project.

De output hiervan genereert een BUNCH van nieuwe markerdown-files in de doel talen. Dit is een super eenvoudige manier om een blogpost in meerdere talen te laten vertalen.

[Vertaalde posten](/translatedposts.png)

[TOC]

## Voorwaarden

Een installering van EasyNMT is nodig om deze tutorial te volgen. Ik draai het meestal als een Dockerdienst. Je kunt de installatieinstructies vinden. [Hier.](https://github.com/UKPLab/EasyNMT/blob/main/docker/README.md) Die omvat hoe het als een dockerdienst te runnen.

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

Of als je een NVIDIA GPU hebt:

```shell
docker run -d -p 24080:80 --env MAX_WORKERS_BACKEND=6 --env MAX_WORKERS_FRONTEND=6 easynmt/api:2.0.2-cuda11.3
```

De MAX_WORKERS_BACKEND en MAX _WOSKERS_FRONTEND omgevingsvariaties bepalen het aantal werknemers dat EasyNMT zal gebruiken.

Let op dat EasyNMT geen SMOOTHEST-dienst is om te runnen, maar het is de beste die ik voor dit doel heb gevonden.

Oh en het vertaalde ook 'Conclusie' naar wat nonsens over het innoveren van de voorstel aan de EU

## Naïeve benadering van laad Balancing

Easy NMT is een hongerige beest als het om middelen gaat, dus in mijn MarkdownTranslatorService heb ik een super eenvoudig willekeurige IP-kieser die gewoon roteert door de lijst van IP's van een lijst van machines die ik gebruik om EasyNMT te runnen.

In eerste instantie komt dit op de `model_name` methode op de EasyNMT-dienst, dit is een snelle, eenvoudige manier om te controleren of de dienst opstaatM SK2 Als het weliswaar is , voegt hij de IP toe aan een lijst van werkende IP'sMSC4 Als dat niet isMST6 voegen ze het niet toe aan de lijst

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

Dan binnen de `Post` methode van `MarkdownTranslatorService` We roteren door de werkende IP's om de volgende te vinden.

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

Dit is een super eenvoudige manier om de verzoeken over een aantal machines te balanceren.

De schmick. ` currentIPIndex = (currentIPIndex + 1) % IPs.Length;` draait gewoon door de lijst van IP's, beginnend bij 0 en gaand naar de lengte van de lijst.

## Een Markdown-lêer vertalen

Dit is de code die ik in de MarkdownTranslatorService heb.

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

Zoals je ziet heeft het een aantal stappen:

1. `  var document = Markdig.Markdown.Parse(markdown);` - Dit berekent de merkdown-strijd in een document.
2. `  var textStrings = ExtractTextStrings(document);` - Dit haalt de tekstdraden uit het document.
   Dit gebruikt de methode.

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

Dit controleert of het 'woord' echt een werk is, en dat beeldnames de zinsverdelingsfunciteit in EasyNMT kunnen verstoren.

3. `  var batchSize = 10;` - Dit bepaalt de grootte van het lot voor de vertalingsdienst. EasyNMT heeft een beperking op het aantal woorden die hij in één keer kan vertalen omheen \500, dus \ 10\ lijnen zijn hier meestal een goede grootte voor het lot.
4. `csharp await Post(batch, targetLang, cancellationToken)`
   Dit roept in de methode die dan het lot naar de EasyNMT-dienst stuurt.

```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());` - Dit herinvoegt de vertaalde stringen terug in het document.

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

## Gehosteerde Diensten

Om dit allemaal te doen, gebruik ik een IHostedLifetimeService die in de Program gestart is.cs-file. Deze dienst leest in een markerdown-file≥, vertaalt het naar een aantal talen en zet de vertaalde files op disket.

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

Zoals je ziet, controleert het ook de haas van de lêer om te zien of het veranderd is.

Dit gebeurt door een snelle haas te compeneren van de oorspronkelijke merkdown-file en dan te testen of die lêer veranderd is voordat je het probeert te vertalen.

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

De opstelling in het programma.cs is vrij simpel:

```csharp

    builder.Services.AddHostedService<BackgroundTranslateService>();
services.AddHttpClient<MarkdownTranslatorService>(options =>
{
    options.Timeout = TimeSpan.FromMinutes(15);
});
```

Ik stelde de HostedService (BackgroundTranslateService) en de HttpClient voor de MarkdownTranslatorService . op.
Een gehosteerde dienst is een lang, in de achtergrond draaiend service.. Het is een goede plek om diensten die continu in de background moeten draaien of wat tijd nodig hebben om te voltooien.. De nieuwe interface van IHostedLifetimeService is een beetje flexibeler dan de oude interface en laat ons de taken volledig in de Hintergrund makkelijker uitvoeren dan de oudere interface.

Hier kan je zien dat I'm de tijdverstreke van HttpClient op 15 minuten legt.

## In slotting:

Dit is een vrij eenvoudige manier om een merkdown-file te vertalen naar meerdere talen. `MarkdownBlogService` om de vertaalde namen voor elke blogpost te trekken.