feat: Imagen → Gemini Flash Image (gemini-2.5-flash-image)

- Gemini kann Image-to-Image (Cover+Landschaft), Imagen nur Text
- generateContent statt predict, inlineData statt rawBytes
- PNG-Output, optionale PIL-Konvertierung zu JPEG
- image_base64 als Referenzbild im contents[].parts[]
- Getestet: Text-to-Image + Image-to-Image
This commit is contained in:
Hermes Agent
2026-06-23 08:38:56 +02:00
parent 23a4bff254
commit 202cb9da42
+55 -49
View File
@@ -66,26 +66,20 @@ MONTHS = {
"September": 9, "Oktober": 10, "November": 11, "Dezember": 12
}
# ── Google Imagen API Configuration ────────────────────────────────────────
IMAGEN_API_URL = (
# ── Gemini Flash Image API Configuration ────────────────────────────────────
GEMINI_IMAGE_MODEL = "gemini-2.5-flash-image"
GEMINI_API_URL = (
"https://generativelanguage.googleapis.com/v1beta/"
"models/imagen-4.0-generate-001:predict"
"models/" + GEMINI_IMAGE_MODEL + ":generateContent"
)
IMAGEN_KEY_SECRET = "bsn/google-imagen-key"
# Resolution Mapping (Imagen 4.0 verwendet 1K/2K direkt)
IMAGEN_SIZE_MAP = {
"1K": "1K",
"2K": "2K",
"4K": "2K", # 4K nicht unterstützt → falle zurück auf 2K
}
GEMINI_KEY_SECRET = "bsn/google-imagen-key"
def _get_imagen_key() -> str:
"""Google Imagen API-Key aus bsn-secrets lesen."""
def _get_gemini_key() -> str:
"""Google Gemini API-Key aus bsn-secrets lesen."""
try:
r = subprocess.run(
["/home/hermes/bin/bsn-secrets", "get", IMAGEN_KEY_SECRET],
["/home/hermes/bin/bsn-secrets", "get", GEMINI_KEY_SECRET],
capture_output=True, text=True, timeout=5
)
return r.stdout.strip()
@@ -93,44 +87,57 @@ def _get_imagen_key() -> str:
return ""
def _imagen_generate(
def _gemini_generate_image(
prompt: str,
image_base64: str | None,
aspect_ratio: str,
resolution: str
) -> dict:
"""Google Imagen aufrufen — synchron, liefert Base64-Bild direkt zurück.
"""Gemini Flash Image aufrufen — Text-to-Image und Image-to-Image.
Args:
prompt: Text-Prompt für die Bildgenerierung.
prompt: Text-Prompt.
image_base64: Optionales Referenzbild als Base64 (Image-to-Image).
aspect_ratio: Seitenverhältnis (z.B. "16:9").
resolution: Auflösung ("1K", "2K", "4K").
"""
key = _get_imagen_key()
if not key:
return {"error": "Kein Google Imagen API-Key"}
resolution: Behält Kompatibilität (von Gemini ignoriert).
instance = {"prompt": prompt}
Returns:
dict mit "image_base64" (PNG) oder "error".
"""
key = _get_gemini_key()
if not key:
return {"error": "Kein Gemini API-Key"}
parts = [{"text": f"{prompt}\n\nAspect ratio: {aspect_ratio}."}]
if image_base64:
instance["rawBytes"] = image_base64
parts.append({
"inlineData": {
"mimeType": "image/jpeg",
"data": image_base64
}
})
body = json.dumps({
"instances": [instance],
"parameters": {
"sampleCount": 1,
"aspectRatio": aspect_ratio,
"sampleImageSize": IMAGEN_SIZE_MAP.get(resolution, "2K"),
"contents": [{"parts": parts}],
"generationConfig": {
"responseModalities": ["image", "text"]
}
}).encode()
req = urllib.request.Request(IMAGEN_API_URL, data=body, method="POST")
req = urllib.request.Request(GEMINI_API_URL, data=body, method="POST")
req.add_header("x-goog-api-key", key)
req.add_header("Content-Type", "application/json")
try:
with urllib.request.urlopen(req, timeout=30) as resp:
return json.loads(resp.read())
with urllib.request.urlopen(req, timeout=45) as resp:
result = json.loads(resp.read())
candidates = result.get("candidates") or []
for cand in candidates:
for part in cand.get("content", {}).get("parts", []):
inline = part.get("inlineData")
if inline and inline.get("data"):
return {"image_base64": inline["data"]}
return {"error": "Kein Bild in Gemini-Antwort"}
except urllib.error.HTTPError as e:
return {"error": f"HTTP {e.code}: {e.read().decode()[:300]}"}
@@ -1031,46 +1038,45 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
self._json_reply(400, {"ok": False, "error": "Prompt fehlt"})
return
# Google Imagen aufrufen (synchron!)
result = _imagen_generate(prompt, image_base64, aspect_ratio, resolution)
# Gemini Flash Image aufrufen (synchron!)
result = _gemini_generate_image(prompt, image_base64, aspect_ratio, resolution)
if "error" in result:
self._json_reply(500, {"ok": False, "error": result["error"]})
return
# Base64-Bild extrahieren
predictions = result.get("predictions") or []
if not predictions:
self._json_reply(500, {"ok": False, "error": "Kein Bild in Imagen-Antwort"})
return
b64_data = predictions[0].get("bytesBase64Encoded", "")
# Base64-Bild (PNG) dekodieren
b64_data = result.get("image_base64", "")
if not b64_data:
self._json_reply(500, {"ok": False, "error": "Leeres Base64-Bild"})
self._json_reply(500, {"ok": False, "error": "Leeres Bild von Gemini"})
return
img_bytes = b64.b64decode(b64_data)
# Im media-Ordner speichern (wie normale Uploads)
# Im media-Ordner speichern
now = datetime.now()
month_dir = os.path.join(MEDIA_DIR, now.strftime("%Y"), now.strftime("%m"))
os.makedirs(month_dir, exist_ok=True)
filename = f"imagen_{now.strftime('%H%M%S')}.jpg"
filepath = os.path.join(month_dir, filename)
base_name = f"gemini_{now.strftime('%H%M%S')}"
ext = ".png" # Gemini liefert PNG
filepath = os.path.join(month_dir, base_name + ext)
with open(filepath, "wb") as f:
f.write(img_bytes)
# Optional: PIL-Normalisierung auf 1920x1080 @ 72 DPI
# Optional: PIL-Konvertierung zu JPEG (1920x1080 @ 72 DPI)
try:
from PIL import Image
img = Image.open(filepath)
img = img.convert("RGB")
img = img.resize((1920, 1080), Image.LANCZOS)
img.save(filepath, "JPEG", dpi=(72, 72), quality=92)
jpg_path = os.path.join(month_dir, base_name + ".jpg")
img.save(jpg_path, "JPEG", dpi=(72, 72), quality=92)
filepath = jpg_path
ext = ".jpg"
except Exception:
pass # Original behalten wenn PIL fehlschlägt
pass # PNG behalten wenn PIL fehlschlägt
img_url = f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{filename}?key={ACCESS_TOKEN}"
img_url = f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{base_name}{ext}?key={ACCESS_TOKEN}"
self._json_reply(200, {
"ok": True,
"imageUrl": img_url,