From 202cb9da42535361abc7fb66f96b0f47b36d1d7e Mon Sep 17 00:00:00 2001 From: Hermes Agent Date: Tue, 23 Jun 2026 08:38:56 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Imagen=20=E2=86=92=20Gemini=20Flash=20I?= =?UTF-8?q?mage=20(gemini-2.5-flash-image)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- article_server.py | 100 ++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 47 deletions(-) diff --git a/article_server.py b/article_server.py index e27b876..3e84dfd 100644 --- a/article_server.py +++ b/article_server.py @@ -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"). + resolution: Behält Kompatibilität (von Gemini ignoriert). + + Returns: + dict mit "image_base64" (PNG) oder "error". """ - key = _get_imagen_key() + key = _get_gemini_key() if not key: - return {"error": "Kein Google Imagen API-Key"} + return {"error": "Kein Gemini API-Key"} - instance = {"prompt": prompt} + 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;} 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,