# Att översätta markerdown-filer automatiskt med EasyNMT

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

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

EasyNMT är en lokalt installerad tjänst som erbjuder en enkel gränssnitt till ett antal maskinöversättningstjänster.

Du kan hitta alla lêer för den här instruktionen i [GitHubs arkiv](https://github.com/scottgal/mostlylucidweb/tree/main/Mostlylucid/MarkdownTranslator) för detta projekt.

Utgången av detta skapade en BUNCH av nya markerdown-filer i målspråket. Det här är ett super enkelt sätt att få en bloggpost översatt till flera språk

[översatta poster](/translatedposts.png)

[TOC]

## Förberedelser

En installering av EasyNMT är nödvändig för att följa den här instruktionen. Jag driver den vanligtvis som en Docker-service. Du kan hitta instruktionerna [här](https://github.com/UKPLab/EasyNMT/blob/main/docker/README.md) som innefattar hur man kör den som en dockerservice.

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

Eller om du har en NVIDIA GPU tillgänglig:

```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_WORKERS_BACKEND och MAX _WOSKERS_FRONTEND miljövariabler sätter upp antalet arbetare som EasyNMT kommer att använda

NOTA: EasyNMT är inte den MOOTHEST-Diensten att köra, men det är den bästa jag hittat för detta.

Och den översatte också ' slutsatsen ' till lite nonsens om att föra fram förslaget till EU ... att bedräga det

## En naiv tillvägagångspunkt för Last Balancing

Easy NMT är ett hungrande djur när det kommer till resurser, så i min MarkdownTranslatorService har jag en super enkel slumpmässig IP-selektor som bara roterar genom listan av IPs på en lista med maskiner som jag använder för att köra EasyNMT.

Till att börja med får vi en `model_name` metod på EasyNMT-tjänsterna, det här är ett snabbt, enkelt sätt att kolla om tjänsten är uppkopplad . Om det ärM SK3 så lägger den till IP: n i listan med fungerande IP' s

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

Sedan inom `Post` metoden `MarkdownTranslatorService` vi roterar genom de fungerande IPs för att hitta nästa

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

Det här är ett super enkelt sätt att ladda upp räkningar över ett antal maskiner.

Schmick ` currentIPIndex = (currentIPIndex + 1) % IPs.Length;` roterar genom listan av IP som börjar med 0 och går till listans längd

## Att översätta en markerad file

Det här är koden jag har i MarkdownTranslatorService.

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

Som ni kan se har den ett antal steg:

1. `  var document = Markdig.Markdown.Parse(markdown);` - Det här tolkar markerdown-raden till ett dokument
2. `  var textStrings = ExtractTextStrings(document);` - Det här extraherar textsträngarna från dokumentet
   Det här använder metoden

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

Det här kontrollerar om 'ord ' är verkligen ett arbete

3. `  var batchSize = 10;` - Det här ställer upp volymen för översättningstjänsterna . EasyNMT har en gräns på antalet ord som den kan översätta på en gång M SK2 runtomkring ♫ 500, så ♫ ♫ 10 ♫ linjer är i allmänhet en bra volym här ♫
4. `csharp await Post(batch, targetLang, cancellationToken)`
   Detta aktiverar metoden som skickar ut paketet till EasyNMT-tjänsterna.

```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());` - Detta återinser översatta strängar tillbaks i dokumentet.

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

## Hemmade dienste

För att köra allt detta använder jag en IHostedLifetimeService som startades i Programmen.cs-datet. Den här tjänsterna läser i en markerdown-Datet , översätter den till ett antal språk och skriver ut de översatta lêerna på disketM SK3

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

Som ni kan se så kontrollerar den också filens hash för att se om det ändrats.

Detta görs genom att räkna ut en snabb hash av den ursprungliga markerdown-fiilen och sedan testa om den har ändrats innan man försöker översätta den.

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

Programmets uppsättning är ganska enkel

```csharp

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

Jag satte upp HostedService (BackgroundTranslateService) och HttpClient för MarkdownTranslatorService .
En Hosted Service är en lång ”-” -drivande service som kör i bakgrunden.“ . ” Det ” ' ” är ett bra ställe att lägga till tjänster som behöver fungera kontinuerligt på bakgrunden eller som bara tar ett tag för att färdigställas. “ . ” Den nya IHostedLifetimeService-gränssnittet är lite mer flexibelt än den gamla IHosedService- gränssnittet och låter oss utföra uppgifter fullständigt på baksidan mycket enklare än den äldre IHotedService.

Här kan ni se att I'm sätter utgången för HttpClient till 15 minuter . Det beror på att EasyNMT kan vara lite långsamt med att svara | ( | speciellt den första gången man använder ett språkmodell |). | ISk5 | m sätter också basenadressen till maskinens IP-adress som driver EasyN MT dienste |

## I slutsats

Det här är ett ganska enkelt sätt att översätta en markerdown-plik till flera språk. `MarkdownBlogService` att dra översatta namn för varje bloggpost.