using FictionArchive.Service.TranslationService.Models; using FictionArchive.Service.TranslationService.Models.Enums; using Polly; using Language = FictionArchive.Common.Enums.Language; namespace FictionArchive.Service.TranslationService.Services.TranslationEngines.NanoGpt; public class NanoGptTranslationEngine : ITranslationEngine { private const double Temperature = 0.2; private const string SystemPromptTemplate = "You are a professional translator. Translate the user's message from {0} to {1}. " + "The source text is wrapped in tags; translate only the contents of those tags. " + "Preserve formatting (paragraphs, line breaks, punctuation). " + "Do not add commentary, notes, or alternative translations. " + "Output only the translated text, with no surrounding tags."; private readonly NanoGptApiClient _apiClient; private readonly NanoGptModelOptions _options; private readonly ResiliencePipeline _pipeline; private readonly ILogger _logger; public NanoGptTranslationEngine( NanoGptApiClient apiClient, NanoGptModelOptions options, ResiliencePipeline pipeline, ILogger logger) { _apiClient = apiClient; _options = options; _pipeline = pipeline; _logger = logger; } public TranslationEngineDescriptor Descriptor => new() { DisplayName = _options.DisplayName, Key = _options.Key }; public async Task GetTranslation(string body, Language from, Language to) { var systemPrompt = string.Format(SystemPromptTemplate, GetLanguageName(from), GetLanguageName(to)); var userPrompt = $"\n{body}\n"; var messages = new List { new("system", systemPrompt), new("user", userPrompt) }; try { var response = await _pipeline.ExecuteAsync( async ct => await _apiClient.CompleteAsync(_options.Model, messages, Temperature, ct)); if (response.Choices.Count == 0) { _logger.LogWarning("NanoGPT returned no choices for model {Model}", _options.Model); return Failed(body, from, to); } _logger.LogInformation( "Translated text via {Model}. Tokens billed: {TotalTokens} (prompt {Prompt}, completion {Completion})", _options.Model, response.Usage.TotalTokens, response.Usage.PromptTokens, response.Usage.CompletionTokens); return new TranslationResult { OriginalText = body, From = from, To = to, TranslationEngineKey = _options.Key, BilledCharacterCount = (uint)response.Usage.TotalTokens, Status = TranslationRequestStatus.Success, TranslatedText = response.Choices[0].Message.Content }; } catch (Exception ex) { _logger.LogError(ex, "NanoGPT translation failed for model {Model} ({From} to {To})", _options.Model, from, to); return Failed(body, from, to); } } private TranslationResult Failed(string body, Language from, Language to) => new() { OriginalText = body, From = from, To = to, TranslationEngineKey = _options.Key, Status = TranslationRequestStatus.Failed, TranslatedText = null, BilledCharacterCount = 0 }; private static string GetLanguageName(Language language) => language switch { Language.En => "English", Language.Kr => "Korean", Language.Ch => "Chinese", Language.Ja => "Japanese", _ => throw new ArgumentOutOfRangeException(nameof(language), language, null) }; }