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:
+53
-47
@@ -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;}</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,
|
||||
|
||||
Reference in New Issue
Block a user