Files
gott/Services/KreaService.cs
T
MelekiandClaude Opus 4.8 1641bf11b6 Fix Krea image generation by removing unsupported batchSize
Krea removed the batchSize parameter ("Generation batch size is no longer
supported, submit one request per output"), so every image request failed
with HTTP 422 — breaking session/agent avatar generation. Drop batchSize
from the payload and instead send one request per requested image in a
loop, collecting all returned jobs. Endpoint paths, Bearer auth and base
URL are unchanged.

- KreaService: remove batchSize from payload
- KreaService: loop record.BatchSize times, one POST per image, aggregate
  jobs (handles both array and single-object responses)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:14:30 +02:00

317 lines
13 KiB
C#

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 ServerSettingsService _settings;
private readonly string _imagesDir;
private string _token => _settings.Get("KreaToken");
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" },
{ "z-image", "/generate/image/z-image/z-image" },
{ "qwen-2512", "/generate/image/qwen/2512" },
{ "nano-banana-2", "/generate/image/google/nano-banana-2" },
{ "seedream-5-lite", "/generate/image/bytedance/seedream-5-lite" },
{ "seededit", "/generate/image/bytedance/seededit" },
};
// Models that don't support width/height/steps/guidance
private static readonly HashSet<string> MINIMAL_MODELS = new HashSet<string>
{
"seedream-3", "runway-gen-4", "seedream-5-lite", "seededit"
};
public bool IsConfigured => !string.IsNullOrWhiteSpace(_token);
public KreaService(IHttpClientFactory httpFactory, ServerSettingsService settings, string dataDir)
{
_httpFactory = httpFactory;
_settings = settings;
_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
// Hinweis: Krea unterstützt 'batchSize' nicht mehr ("batch size is no longer supported,
// submit one request per output") -> wir senden pro gewünschtem Bild eine eigene Anfrage.
var payload = new Dictionary<string, object>();
payload["prompt"] = prompt;
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);
// Eine Anfrage pro gewünschtem Bild (batchSize wird serverseitig nicht mehr akzeptiert).
var jobs = new List<JsonNode>();
for (int b = 0; b < record.BatchSize; b++)
{
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 node = JsonNode.Parse(responseBody);
if (node is JsonArray arr)
jobs.AddRange(arr.Where(j => j != null));
else if (node != null)
jobs.Add(node);
}
// 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;
}
}
}