diff --git a/article_server.py b/article_server.py index 42ff928..ced6e75 100644 --- a/article_server.py +++ b/article_server.py @@ -66,68 +66,63 @@ MONTHS = { "September": 9, "Oktober": 10, "November": 11, "Dezember": 12 } -# ── NanoBanana API Configuration ────────────────────────────────────────── -NANOBANANA_API_URL = "https://api.nanobananaapi.ai" -NANOBANANA_UPLOAD_DIR = "/tmp/nanobanana_uploads" -os.makedirs(NANOBANANA_UPLOAD_DIR, exist_ok=True) +# ── Google Imagen API Configuration ──────────────────────────────────────── +IMAGEN_API_URL = ( + "https://generativelanguage.googleapis.com/v1beta/" + "models/imagen-4.0-generate-001:predict" +) +IMAGEN_KEY_SECRET = "bsn/google-imagen-key" -def _get_nanobanana_key() -> str: - """NanoBanana API-Key aus bsn-secrets lesen.""" +# 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 +} + + +def _get_imagen_key() -> str: + """Google Imagen API-Key aus bsn-secrets lesen.""" try: r = subprocess.run( - ["/home/hermes/bin/bsn-secrets", "get", "bsn/nanobanana-key"], + ["/home/hermes/bin/bsn-secrets", "get", IMAGEN_KEY_SECRET], capture_output=True, text=True, timeout=5 ) return r.stdout.strip() except Exception: - return os.environ.get("NANOBANANA_API_KEY", "") + return "" -def _nanobanana_generate(prompt: str, image_urls: list, aspect_ratio: str, resolution: str) -> dict: - """NanoBanana generate-2 API aufrufen.""" - key = _get_nanobanana_key() + +def _imagen_generate( + prompt: str, + image_urls: list, + aspect_ratio: str, + resolution: str +) -> dict: + """Google Imagen aufrufen — synchron, liefert Base64-Bild direkt zurück.""" + key = _get_imagen_key() if not key: - return {"error": "Kein API-Key"} - - data = json.dumps({ - "prompt": prompt, - "imageUrls": image_urls if image_urls else [], - "aspectRatio": aspect_ratio, - "resolution": resolution, - "outputFormat": "jpg", + return {"error": "Kein Google Imagen API-Key"} + + body = json.dumps({ + "instances": [{"prompt": prompt}], + "parameters": { + "sampleCount": 1, + "aspectRatio": aspect_ratio, + "sampleImageSize": IMAGEN_SIZE_MAP.get(resolution, "2048x2048"), + } }).encode() - - req = urllib.request.Request( - f"{NANOBANANA_API_URL}/api/v1/nanobanana/generate-2", - data=data, method="POST" - ) - req.add_header("Authorization", "Bearer " + key) + + req = urllib.request.Request(IMAGEN_API_URL, data=body, method="POST") + req.add_header("x-goog-api-key", key) req.add_header("Content-Type", "application/json") - req.add_header("User-Agent", "Mozilla/5.0") # Cloudflare-Workaround - + try: with urllib.request.urlopen(req, timeout=30) as resp: return json.loads(resp.read()) except urllib.error.HTTPError as e: return {"error": f"HTTP {e.code}: {e.read().decode()[:300]}"} -def _nanobanana_status(task_id: str) -> dict: - """NanoBanana Task-Status abrufen.""" - key = _get_nanobanana_key() - if not key: - return {"error": "Kein API-Key"} - - req = urllib.request.Request( - f"{NANOBANANA_API_URL}/api/v1/nanobanana/record-info?taskId={task_id}" - ) - req.add_header("Authorization", "Bearer " + key) - req.add_header("User-Agent", "Mozilla/5.0") - - try: - with urllib.request.urlopen(req, timeout=15) as resp: - return json.loads(resp.read()) - except urllib.error.HTTPError as e: - return {"error": f"HTTP {e.code}"} - def _get_unpublished_articles() -> list[dict]: """Liste aller unveröffentlichten Artikel (ohne joomla-id).""" articles = [] @@ -885,8 +880,6 @@ h1{color:#c0392b;}p{color:#888;} self.wfile.write(html.encode("utf-8")) elif path == "/nanobanana/articles": self._handle_nanobanana_articles() - elif path == "/nanobanana/status": - self._handle_nanobanana_status() elif path.startswith("/media/"): rel = path[len("/"):] filepath = os.path.normpath(os.path.join(WORKSPACE, rel)) @@ -955,11 +948,7 @@ h1{color:#c0392b;}p{color:#888;} elif path == "/nanobanana/generate": if not self._check_token(): return - self._handle_nanobanana_generate() - elif path == "/nanobanana/status": - if not self._check_token(): - return - self._handle_nanobanana_status() + self._handle_image_generate() elif path == "/nanobanana/articles": if not self._check_token(): return @@ -981,31 +970,30 @@ h1{color:#c0392b;}p{color:#888;} self.end_headers() # ═══════════════════════════════════════════════════════════════════ - # NanoBanana Handler + # Imagen Handler (Google Imagen — synchron, kein Polling) # ═══════════════════════════════════════════════════════════════════ - def _handle_nanobanana_generate(self): - """NanoBanana Bildgenerierung starten.""" + def _handle_image_generate(self): + """Bildgenerierung via Google Imagen (synchron, Base64→JPEG).""" content_type = self.headers.get("Content-Type", "") if "multipart/form-data" not in content_type: self._json_reply(400, {"ok": False, "error": "Expected multipart/form-data"}) return - + boundary = content_type.split("boundary=")[-1].strip() if not boundary: self._json_reply(400, {"ok": False, "error": "Kein Boundary"}) return - + length = int(self.headers.get("Content-Length", 0)) raw = self.rfile.read(length) - + # Multipart manuell parsen (Python 3.13 hat kein cgi) parts = raw.split(b"--" + boundary.encode()) prompt = "" - image_urls = [] aspect_ratio = "16:9" - resolution = "2K" - + resolution = "1K" + for part in parts: if b"Content-Disposition" not in part: continue @@ -1013,71 +1001,64 @@ h1{color:#c0392b;}p{color:#888;} if headers_end == -1: continue headers_raw = part[:headers_end].decode("utf-8", errors="ignore") - content = part[headers_end + 4:] - content = content.rstrip(b"\r\n") - + content = part[headers_end + 4:].rstrip(b"\r\n") + if 'name="prompt"' in headers_raw: prompt = content.decode("utf-8", errors="ignore") elif 'name="aspectRatio"' in headers_raw: aspect_ratio = content.decode("utf-8", errors="ignore") elif 'name="resolution"' in headers_raw: resolution = content.decode("utf-8", errors="ignore") - elif 'name="image"' in headers_raw and len(content) > 100: - # Bild als Data-URL für die NanoBanana API - import base64 as b64 - b64_data = b64.b64encode(content).decode() - ct = "image/jpeg" - if b"Content-Type" in part: - ct_line = [l for l in headers_raw.split("\r\n") if "Content-Type" in l] - if ct_line: - ct = ct_line[0].split(":")[-1].strip() - image_urls.append(f"data:{ct};base64,{b64_data}") - + if not prompt: self._json_reply(400, {"ok": False, "error": "Prompt fehlt"}) return - - result = _nanobanana_generate(prompt, image_urls, aspect_ratio, resolution) - # Prüfe auf Transport-Fehler (Netzwerk/HTTP) - if "error" in result: - self._json_reply(500, {"ok": False, "error": result["error"]}) - return - # Prüfe auf NanoBanana API-Fehler (code != 0, z.B. 402 = kein Guthaben) - api_code = result.get("code", 0) - if api_code != 0: - api_msg = result.get("msg", "Unbekannter API-Fehler") - self._json_reply(500, {"ok": False, "error": f"NanoBanana: {api_msg} (Code {api_code})"}) - return - # Null-Safe: data kann None sein - data = result.get("data") or {} - task_id = data.get("taskId", "") - if not task_id: - self._json_reply(500, {"ok": False, "error": "Keine taskId in API-Antwort"}) - return - self._json_reply(200, {"ok": True, "taskId": task_id}) - def _handle_nanobanana_status(self): - """NanoBanana Task-Status abrufen.""" - parsed = urllib.parse.urlparse(self.path) - params = urllib.parse.parse_qs(parsed.query) - task_id = params.get("taskId", [""])[0] - - if not task_id: - self._json_reply(400, {"ok": False, "error": "taskId fehlt"}) - return - - result = _nanobanana_status(task_id) + # Google Imagen aufrufen (synchron!) + result = _imagen_generate(prompt, [], aspect_ratio, resolution) if "error" in result: self._json_reply(500, {"ok": False, "error": result["error"]}) return - - # Null-Safe: data kann None sein - data = result.get("data") or {} + + # 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", "") + if not b64_data: + self._json_reply(500, {"ok": False, "error": "Leeres Base64-Bild"}) + return + + import base64 as b64 + img_bytes = b64.b64decode(b64_data) + + # Im media-Ordner speichern (wie normale Uploads) + 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) + + with open(filepath, "wb") as f: + f.write(img_bytes) + + # Optional: PIL-Normalisierung auf 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) + except Exception: + pass # Original behalten wenn PIL fehlschlägt + + img_url = f"/media/{now.strftime('%Y')}/{now.strftime('%m')}/{filename}?key={ACCESS_TOKEN}" self._json_reply(200, { "ok": True, - "successFlag": data.get("successFlag"), - "resultImageUrl": (data.get("response") or {}).get("resultImageUrl"), - "completeTime": data.get("completeTime"), + "imageUrl": img_url, + "filepath": filepath, }) def _handle_nanobanana_articles(self): @@ -1107,12 +1088,21 @@ h1{color:#c0392b;}p{color:#888;} self._json_reply(400, {"ok": False, "error": "article und imageUrl erforderlich"}) return - # Bild herunterladen + # Bild herunterladen oder lokal lesen try: - req = urllib.request.Request(image_url) - req.add_header("User-Agent", "Mozilla/5.0") - with urllib.request.urlopen(req, timeout=30) as resp: - img_data = resp.read() + if image_url.startswith("/media/"): + # Lokales Bild — direkt von Platte lesen + local_path = os.path.join(WORKSPACE, image_url.lstrip("/")) + if not os.path.isfile(local_path): + self._json_reply(500, {"ok": False, "error": f"Lokales Bild nicht gefunden: {local_path}"}) + return + with open(local_path, "rb") as lf: + img_data = lf.read() + else: + req = urllib.request.Request(image_url) + req.add_header("User-Agent", "Mozilla/5.0") + with urllib.request.urlopen(req, timeout=30) as resp: + img_data = resp.read() except Exception as e: self._json_reply(500, {"ok": False, "error": f"Download fehlgeschlagen: {e}"}) return @@ -1671,7 +1661,7 @@ NANOBANANA_HTML = """
-