using DeepL; using FictionArchive.Common.Extensions; using FictionArchive.Service.Shared; using FictionArchive.Service.Shared.Extensions; using FictionArchive.Service.Shared.Services.GraphQL; using FictionArchive.Service.TranslationService.Consumers; using FictionArchive.Service.TranslationService.GraphQL; using FictionArchive.Service.TranslationService.Services; using FictionArchive.Service.TranslationService.Services.Database; using FictionArchive.Service.TranslationService.Services.TranslationEngines; using FictionArchive.Service.TranslationService.Services.TranslationEngines.DeepLTranslate; using System.Net.Http.Headers; using FictionArchive.Service.TranslationService.Services.Resilience; using FictionArchive.Service.TranslationService.Services.TranslationEngines.NanoGpt; using Polly; namespace FictionArchive.Service.TranslationService; public class Program { public static void Main(string[] args) { var isSchemaExport = SchemaExportDetector.IsSchemaExportMode(args); var builder = WebApplication.CreateBuilder(args); builder.AddLocalAppsettings(); builder.Services.AddHealthChecks(); #region MassTransit if (!isSchemaExport) { builder.Services.AddFictionArchiveMassTransit( builder.Configuration, x => { x.AddConsumer(); }); } #endregion #region Database builder.Services.RegisterDbContext( builder.Configuration.GetConnectionString("DefaultConnection"), skipInfrastructure: isSchemaExport); #endregion #region GraphQL builder.Services.AddDefaultGraphQl() .AddAuthorization(); #endregion #region Translation Adapter // Shared resilience pipeline used by both DeepL and NanoGPT engines. builder.Services.AddSingleton(TranslationResiliencePipeline.Build()); // DeepL builder.Services.AddTransient(_ => new DeepLClient(builder.Configuration["DeepL:ApiKey"])); builder.Services.AddTransient(); // NanoGPT — typed HttpClient (bearer auth + base address + 120s timeout). builder.Services.AddHttpClient(client => { var baseAddress = builder.Configuration["NanoGpt:BaseAddress"]; if (!string.IsNullOrWhiteSpace(baseAddress)) { client.BaseAddress = new Uri(baseAddress); } var apiKey = builder.Configuration["NanoGpt:ApiKey"]; if (!string.IsNullOrWhiteSpace(apiKey) && apiKey != "REPLACE_ME") { client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey); } client.Timeout = TimeSpan.FromSeconds(120); }); // Register one ITranslationEngine per configured NanoGPT model. var nanoGptModels = builder.Configuration .GetSection("NanoGpt:Models") .Get() ?? []; foreach (var modelOptions in nanoGptModels) { builder.Services.AddTransient(sp => new NanoGptTranslationEngine( sp.GetRequiredService(), modelOptions, sp.GetRequiredService(), sp.GetRequiredService>())); } builder.Services.AddTransient(); #endregion // Authentication & Authorization builder.Services.AddOidcAuthentication(builder.Configuration); builder.Services.AddFictionArchiveAuthorization(); var app = builder.Build(); // Update database (skip in schema export mode) if (!isSchemaExport) { using var scope = app.Services.CreateScope(); var dbContext = scope.ServiceProvider.GetRequiredService(); dbContext.UpdateDatabase(); } app.UseHttpsRedirection(); app.MapHealthChecks("/healthz"); app.UseAuthentication(); app.UseAuthorization(); app.MapGraphQL(); app.RunWithGraphQLCommands(args); } }