110 lines
3.9 KiB
C#
110 lines
3.9 KiB
C#
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 <source_text> 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<NanoGptTranslationEngine> _logger;
|
|
|
|
public NanoGptTranslationEngine(
|
|
NanoGptApiClient apiClient,
|
|
NanoGptModelOptions options,
|
|
ResiliencePipeline pipeline,
|
|
ILogger<NanoGptTranslationEngine> logger)
|
|
{
|
|
_apiClient = apiClient;
|
|
_options = options;
|
|
_pipeline = pipeline;
|
|
_logger = logger;
|
|
}
|
|
|
|
public TranslationEngineDescriptor Descriptor => new()
|
|
{
|
|
DisplayName = _options.DisplayName,
|
|
Key = _options.Key
|
|
};
|
|
|
|
public async Task<TranslationResult> GetTranslation(string body, Language from, Language to)
|
|
{
|
|
var systemPrompt = string.Format(SystemPromptTemplate, GetLanguageName(from), GetLanguageName(to));
|
|
var userPrompt = $"<source_text>\n{body}\n</source_text>";
|
|
var messages = new List<NanoGptMessage>
|
|
{
|
|
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)
|
|
};
|
|
}
|