diff --git a/Endpoints/KreaEndpoint.cs b/Endpoints/KreaEndpoint.cs new file mode 100644 index 0000000..e490a37 --- /dev/null +++ b/Endpoints/KreaEndpoint.cs @@ -0,0 +1,112 @@ +using System.IO; +using System.Text.Json; +using System.Threading.Tasks; +using Gott.Services; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; + +namespace Gott.Endpoints +{ + public static class KreaEndpoint + { + public static void MapKreaEndpoints(this IEndpointRouteBuilder app) + { + app.MapPost("/api/images/generate", HandleGenerate); + app.MapGet("/api/images", HandleList); + app.MapGet("/api/images/file/{filename}", HandleFile); + app.MapGet("/api/images/status", HandleStatus); + } + + private static async Task HandleGenerate(HttpRequest req, HttpResponse res, KreaService krea) + { + if (!krea.IsConfigured) + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "Krea API nicht konfiguriert (KreaToken fehlt)" }); + return; + } + + JsonElement body; + try { body = await JsonSerializer.DeserializeAsync(req.Body); } + catch + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "Ungültiger JSON-Body" }); + return; + } + + var prompt = body.TryGetProperty("prompt", out var p) ? p.GetString() : null; + if (string.IsNullOrWhiteSpace(prompt)) + { + res.StatusCode = 400; + await res.WriteAsJsonAsync(new { error = "prompt ist erforderlich" }); + return; + } + + var model = body.TryGetProperty("model", out var m) ? m.GetString() : "flux"; + var width = body.TryGetProperty("width", out var w) ? w.GetInt32() : 1024; + var height = body.TryGetProperty("height", out var h) ? h.GetInt32() : 1024; + var steps = body.TryGetProperty("steps", out var st) ? st.GetInt32() : 25; + var guidanceScale = body.TryGetProperty("guidanceScale", out var g) ? g.GetDouble() : 3.0; + var seed = body.TryGetProperty("seed", out var sd) ? sd.GetString() : null; + var batchSize = body.TryGetProperty("batchSize", out var b) ? b.GetInt32() : 1; + + try + { + var result = await krea.GenerateAsync(prompt, model, width, height, steps, guidanceScale, seed, batchSize); + await res.WriteAsJsonAsync(result); + } + catch (System.Exception ex) + { + res.StatusCode = 500; + await res.WriteAsJsonAsync(new { error = ex.Message }); + } + } + + private static async Task HandleList(HttpRequest req, HttpResponse res, KreaService krea) + { + var images = krea.ListImages(); + await res.WriteAsJsonAsync(images); + } + + private static async Task HandleFile(HttpRequest req, HttpResponse res, KreaService krea, string filename) + { + // Sicherheitscheck – kein Path Traversal + if (filename.Contains("..") || filename.Contains("/") || filename.Contains("\\")) + { + res.StatusCode = 400; + return; + } + + var path = krea.GetImagePath(filename); + if (path == null) + { + res.StatusCode = 404; + return; + } + + var ext = Path.GetExtension(filename).ToLower(); + var contentType = ext switch + { + ".png" => "image/png", + ".jpg" => "image/jpeg", + ".jpeg" => "image/jpeg", + ".gif" => "image/gif", + _ => "image/webp", + }; + + res.ContentType = contentType; + await res.SendFileAsync(path); + } + + private static async Task HandleStatus(HttpRequest req, HttpResponse res, KreaService krea) + { + await res.WriteAsJsonAsync(new + { + configured = krea.IsConfigured, + models = KreaService.IMAGE_MODELS.Keys, + }); + } + } +} diff --git a/Models/KreaImageRecord.cs b/Models/KreaImageRecord.cs new file mode 100644 index 0000000..5008858 --- /dev/null +++ b/Models/KreaImageRecord.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace Gott.Models +{ + public class KreaImageRecord + { + public string Id { get; set; } + public string Prompt { get; set; } + public string Model { get; set; } + public int Width { get; set; } + public int Height { get; set; } + public int Steps { get; set; } + public double GuidanceScale { get; set; } + public string Seed { get; set; } + public int BatchSize { get; set; } + public string Status { get; set; } // pending, completed, failed + public List Files { get; set; } = new List(); + public List Urls { get; set; } = new List(); + public string Error { get; set; } + public string CreatedAt { get; set; } + public string CompletedAt { get; set; } + public string MetaFile { get; set; } + } +} diff --git a/Program.cs b/Program.cs index 02242ff..ad10a97 100644 --- a/Program.cs +++ b/Program.cs @@ -52,6 +52,11 @@ builder.Services.AddSingleton(sp => sp.GetRequiredService(), maxIterations)); +// Krea Image Generation +var kreaToken = builder.Configuration["KreaToken"] ?? ""; +builder.Services.AddSingleton(sp => + new KreaService(sp.GetRequiredService(), kreaToken, dataDir)); + // Cronjob-Service (BackgroundService) builder.Services.AddSingleton(); builder.Services.AddHostedService(sp => sp.GetRequiredService()); @@ -74,5 +79,6 @@ app.MapProfileEndpoints(); app.MapSettingsEndpoints(); app.MapTelegramEndpoints(); app.MapCronjobEndpoints(); +app.MapKreaEndpoints(); app.Run("http://0.0.0.0:19000"); diff --git a/Services/KreaService.cs b/Services/KreaService.cs new file mode 100644 index 0000000..84c68c9 --- /dev/null +++ b/Services/KreaService.cs @@ -0,0 +1,299 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Threading.Tasks; +using Gott.Models; + +namespace Gott.Services +{ + public class KreaService + { + private readonly IHttpClientFactory _httpFactory; + private readonly string _token; + private readonly string _imagesDir; + + private const string BASE_URL = "https://api.krea.ai"; + + public static readonly Dictionary IMAGE_MODELS = new Dictionary + { + { "flux", "/generate/image/bfl/flux-1-dev" }, + { "flux-kontext", "/generate/image/bfl/flux-1-kontext-dev" }, + { "flux-1.1-pro", "/generate/image/bfl/flux-1.1-pro" }, + { "flux-1.1-pro-ultra", "/generate/image/bfl/flux-1.1-pro-ultra" }, + { "nano-banana", "/generate/image/google/nano-banana" }, + { "nano-banana-pro", "/generate/image/google/nano-banana-pro" }, + { "imagen-3", "/generate/image/google/imagen-3" }, + { "imagen-4", "/generate/image/google/imagen-4" }, + { "imagen-4-fast", "/generate/image/google/imagen-4-fast" }, + { "imagen-4-ultra", "/generate/image/google/imagen-4-ultra" }, + { "ideogram-2-turbo", "/generate/image/ideogram/ideogram-2-turbo" }, + { "ideogram-3", "/generate/image/ideogram/ideogram-3" }, + { "seedream-3", "/generate/image/bytedance/seedream-3" }, + { "seedream-4", "/generate/image/bytedance/seedream-4" }, + { "chatgpt-image", "/generate/image/openai/gpt-image" }, + { "runway-gen-4", "/generate/image/runway/gen-4" }, + }; + + // Models that don't support width/height/steps/guidance + private static readonly HashSet MINIMAL_MODELS = new HashSet + { + "seedream-3", "runway-gen-4" + }; + + public bool IsConfigured => !string.IsNullOrWhiteSpace(_token); + + public KreaService(IHttpClientFactory httpFactory, string token, string dataDir) + { + _httpFactory = httpFactory; + _token = token; + _imagesDir = Path.Combine(dataDir, "images"); + Directory.CreateDirectory(_imagesDir); + } + + public async Task GenerateAsync( + string prompt, + string model = "flux", + int width = 1024, + int height = 1024, + int steps = 25, + double guidanceScale = 3.0, + string seed = null, + int batchSize = 1) + { + if (!IMAGE_MODELS.TryGetValue(model, out var endpoint)) + throw new ArgumentException($"Unbekanntes Model: {model}. Verfügbar: {string.Join(", ", IMAGE_MODELS.Keys)}"); + + var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var record = new KreaImageRecord + { + Id = timestamp.ToString(), + Prompt = prompt, + Model = model, + Width = width, + Height = height, + Steps = steps, + GuidanceScale = guidanceScale, + Seed = seed, + BatchSize = Math.Max(1, Math.Min(4, batchSize)), + Status = "pending", + CreatedAt = DateTimeOffset.UtcNow.ToString("o"), + }; + + // Build payload + var payload = new Dictionary(); + payload["prompt"] = prompt; + payload["batchSize"] = record.BatchSize; + + if (!MINIMAL_MODELS.Contains(model)) + { + payload["width"] = width; + payload["height"] = height; + payload["steps"] = steps; + payload["guidance_scale_flux"] = guidanceScale; + } + + if (model == "seedream-3") + payload["model"] = "seedream-3-0-t2i-250415"; + + if (!string.IsNullOrEmpty(seed)) + payload["seed"] = seed; + + // Submit job(s) + var client = _httpFactory.CreateClient(); + client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_token}"); + client.DefaultRequestHeaders.Add("Accept", "application/json"); + client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0 (compatible; Gott/1.0)"); + + var json = JsonSerializer.Serialize(payload); + var content = new StringContent(json, Encoding.UTF8, "application/json"); + + var response = await client.PostAsync($"{BASE_URL}{endpoint}", content); + var responseBody = await response.Content.ReadAsStringAsync(); + + if (!response.IsSuccessStatusCode) + throw new Exception($"Krea API Fehler {(int)response.StatusCode}: {responseBody}"); + + var jobsNode = JsonNode.Parse(responseBody); + var jobs = jobsNode is JsonArray arr ? arr.ToList() : new List { jobsNode }; + + // Wait for all jobs + var allUrls = new List(); + var jobResults = new List(); + + foreach (var job in jobs) + { + var jobId = job["job_id"]?.GetValue() + ?? throw new Exception("Kein job_id in Response"); + + var result = await WaitForJobAsync(client, jobId); + var urls = result["result"]?["urls"]?.AsArray(); + if (urls != null) + foreach (var u in urls) + allUrls.Add(u.GetValue()); + + jobResults.Add(result); + } + + // Download images + var savedFiles = await SaveImagesAsync(client, allUrls, timestamp); + + // Save metadata + var metaFile = await SaveMetadataAsync(timestamp, record, allUrls, savedFiles, jobResults); + + record.Status = "completed"; + record.Files = savedFiles; + record.Urls = allUrls; + record.MetaFile = metaFile; + record.CompletedAt = DateTimeOffset.UtcNow.ToString("o"); + + return record; + } + + private async Task WaitForJobAsync(HttpClient client, string jobId, int timeoutSeconds = 120) + { + var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds); + while (DateTime.UtcNow < deadline) + { + var resp = await client.GetAsync($"{BASE_URL}/jobs/{jobId}"); + var body = await resp.Content.ReadAsStringAsync(); + var job = JsonNode.Parse(body); + var status = job["status"]?.GetValue(); + + if (status == "completed") return job; + if (status == "failed") throw new Exception($"Job fehlgeschlagen: {body}"); + if (status == "cancelled") throw new Exception("Job wurde abgebrochen"); + + await Task.Delay(2000); + } + throw new TimeoutException($"Job {jobId} hat nicht innerhalb von {timeoutSeconds}s geantwortet"); + } + + private async Task> SaveImagesAsync(HttpClient client, List urls, long timestamp) + { + var saved = new List(); + for (int i = 0; i < urls.Count; i++) + { + var url = urls[i]; + try + { + var resp = await client.GetAsync(url); + var bytes = await resp.Content.ReadAsByteArrayAsync(); + + // Determine extension + var ext = ".webp"; + var contentType = resp.Content.Headers.ContentType?.MediaType ?? ""; + var extMap = new Dictionary + { + { "image/png", ".png" }, + { "image/jpeg", ".jpg" }, + { "image/webp", ".webp" }, + { "image/gif", ".gif" }, + }; + if (extMap.TryGetValue(contentType, out var mappedExt)) + ext = mappedExt; + + var filename = $"{timestamp}-{i + 1}{ext}"; + var filepath = Path.Combine(_imagesDir, filename); + await File.WriteAllBytesAsync(filepath, bytes); + saved.Add(filename); // Nur Dateiname, kein Pfad + } + catch (Exception ex) + { + Console.WriteLine($"[KreaService] Fehler beim Download von {url}: {ex.Message}"); + } + } + return saved; + } + + private async Task SaveMetadataAsync( + long timestamp, + KreaImageRecord record, + List urls, + List savedFiles, + List jobResults) + { + var meta = new + { + timestamp, + timestamp_iso = DateTimeOffset.FromUnixTimeSeconds(timestamp).ToString("o"), + input = new + { + prompt = record.Prompt, + model = record.Model, + width = record.Width, + height = record.Height, + steps = record.Steps, + guidance_scale = record.GuidanceScale, + seed = record.Seed, + batch_size = record.BatchSize, + }, + output = new + { + urls, + files = savedFiles, + image_count = urls.Count, + }, + jobs = jobResults.Select(j => j.ToJsonString()).ToList(), + }; + + var metaPath = Path.Combine(_imagesDir, $"{timestamp}.json"); + var json = JsonSerializer.Serialize(meta, new JsonSerializerOptions { WriteIndented = true }); + await File.WriteAllTextAsync(metaPath, json); + return $"{timestamp}.json"; + } + + public List ListImages() + { + var result = new List(); + if (!Directory.Exists(_imagesDir)) return result; + + foreach (var metaFile in Directory.GetFiles(_imagesDir, "*.json").OrderByDescending(f => f)) + { + try + { + var json = File.ReadAllText(metaFile); + var node = JsonNode.Parse(json); + var timestamp = node["timestamp"]?.GetValue() ?? 0; + var input = node["input"]; + var output = node["output"]; + + var files = output?["files"]?.AsArray() + .Select(f => f.GetValue()).ToList() ?? new List(); + + result.Add(new KreaImageRecord + { + Id = timestamp.ToString(), + Prompt = input?["prompt"]?.GetValue(), + Model = input?["model"]?.GetValue(), + Width = input?["width"]?.GetValue() ?? 1024, + Height = input?["height"]?.GetValue() ?? 1024, + Steps = input?["steps"]?.GetValue() ?? 25, + GuidanceScale = input?["guidance_scale"]?.GetValue() ?? 3.0, + Seed = input?["seed"]?.GetValue(), + BatchSize = input?["batch_size"]?.GetValue() ?? 1, + Status = "completed", + Files = files, + Urls = output?["urls"]?.AsArray() + .Select(u => u.GetValue()).ToList() ?? new List(), + CreatedAt = node["timestamp_iso"]?.GetValue(), + MetaFile = Path.GetFileName(metaFile), + }); + } + catch { /* ungültige Meta-Datei überspringen */ } + } + + return result; + } + + public string GetImagePath(string filename) + { + var path = Path.Combine(_imagesDir, filename); + return File.Exists(path) ? path : null; + } + } +} diff --git a/appsettings.json b/appsettings.json index 7c6a5e3..2bd9718 100755 --- a/appsettings.json +++ b/appsettings.json @@ -9,5 +9,6 @@ "AnthropicToken": "", "MaxIterations": 100, "PayloadLogging": false, - "PayloadLoggingGzip": false + "PayloadLoggingGzip": false, + "KreaToken": "" }