feat: Krea Image Generation Endpoint

- KreaService: GenerateAsync() mit Job-Polling, Image-Download, Metadaten-Speicherung
- KreaEndpoint: POST /api/images/generate, GET /api/images, GET /api/images/file/{filename}, GET /api/images/status
- KreaImageRecord: Output-Model
- Output: data/images/<timestamp>-<index>.<ext> + data/images/<timestamp>.json
- 16 Modelle: flux, flux-kontext, flux-1.1-pro, imagen-3/4, chatgpt-image, runway-gen-4, etc.
- appsettings.json: KreaToken (Format: KEY_ID:SECRET), obsolete TelegramBotToken/WebhookSecret entfernt
This commit is contained in:
Meleki committed 2026-03-04 18:24:51 +01:00
1 parent f552b44b05
commit 26da25967d
5 files changed
+443 -1

No files matched your search

+299
View File
@@ -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<string, string> IMAGE_MODELS = new Dictionary<string, string>
{
{ "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<string> MINIMAL_MODELS = new HashSet<string>
{
"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<KreaImageRecord> 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<string, object>();
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<JsonNode> { jobsNode };
// Wait for all jobs
var allUrls = new List<string>();
var jobResults = new List<JsonNode>();
foreach (var job in jobs)
{
var jobId = job["job_id"]?.GetValue<string>()
?? 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<string>());
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<JsonNode> 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<string>();
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<List<string>> SaveImagesAsync(HttpClient client, List<string> urls, long timestamp)
{
var saved = new List<string>();
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<string, string>
{
{ "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<string> SaveMetadataAsync(
long timestamp,
KreaImageRecord record,
List<string> urls,
List<string> savedFiles,
List<JsonNode> 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<KreaImageRecord> ListImages()
{
var result = new List<KreaImageRecord>();
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<long>() ?? 0;
var input = node["input"];
var output = node["output"];
var files = output?["files"]?.AsArray()
.Select(f => f.GetValue<string>()).ToList() ?? new List<string>();
result.Add(new KreaImageRecord
{
Id = timestamp.ToString(),
Prompt = input?["prompt"]?.GetValue<string>(),
Model = input?["model"]?.GetValue<string>(),
Width = input?["width"]?.GetValue<int>() ?? 1024,
Height = input?["height"]?.GetValue<int>() ?? 1024,
Steps = input?["steps"]?.GetValue<int>() ?? 25,
GuidanceScale = input?["guidance_scale"]?.GetValue<double>() ?? 3.0,
Seed = input?["seed"]?.GetValue<string>(),
BatchSize = input?["batch_size"]?.GetValue<int>() ?? 1,
Status = "completed",
Files = files,
Urls = output?["urls"]?.AsArray()
.Select(u => u.GetValue<string>()).ToList() ?? new List<string>(),
CreatedAt = node["timestamp_iso"]?.GetValue<string>(),
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;
}
}
}