Att översätta markerdown-filer automatiskt med EasyNMT (Svenska (Swedish))

Att översätta markerdown-filer automatiskt med EasyNMT

Saturday, 03 August 2024

//

6 minute read

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

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 som innefattar hur man kör den som en dockerservice.

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:

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

    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

          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.

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

  1. 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 ♫
  2. csharp await Post(batch, targetLang, cancellationToken) Detta aktiverar metoden som skickar ut paketet till EasyNMT-tjänsterna.
    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;
        }
    }
  1. ReinsertTranslatedStrings(document, translatedStrings.ToArray()); - Detta återinser översatta strängar tillbaks i dokumentet.

    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

  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.

    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


    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.

Finding related posts...
logo

© 2026 Scott Galloway — Unlicense — All content and source code on this site is free to use, copy, modify, and sell.