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
|
"September": 9, "Oktober": 10, "November": 11, "Dezember": 12
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── Google Imagen API Configuration ────────────────────────────────────────
|
# ── Gemini Flash Image API Configuration ────────────────────────────────────
|
||||||
IMAGEN_API_URL = (
|
GEMINI_IMAGE_MODEL = "gemini-2.5-flash-image"
|
||||||
|
GEMINI_API_URL = (
|
||||||
"https://generativelanguage.googleapis.com/v1beta/"
|
"https://generativelanguage.googleapis.com/v1beta/"
|
||||||
"models/imagen-4.0-generate-001:predict"
|
"models/" + GEMINI_IMAGE_MODEL + ":generateContent"
|
||||||
)
|
)
|
||||||
IMAGEN_KEY_SECRET = "bsn/google-imagen-key"
|
GEMINI_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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _get_imagen_key() -> str:
|
def _get_gemini_key() -> str:
|
||||||
"""Google Imagen API-Key aus bsn-secrets lesen."""
|
"""Google Gemini API-Key aus bsn-secrets lesen."""
|
||||||
try:
|
try:
|
||||||
r = subprocess.run(
|
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
|
capture_output=True, text=True, timeout=5
|
||||||
)
|
)
|
||||||
return r.stdout.strip()
|
return r.stdout.strip()
|
||||||
@@ -93,44 +87,57 @@ def _get_imagen_key() -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _imagen_generate(
|
def _gemini_generate_image(
|
||||||
prompt: str,
|
prompt: str,
|
||||||
image_base64: str | None,
|
image_base64: str | None,
|
||||||
aspect_ratio: str,
|
aspect_ratio: str,
|
||||||
resolution: str
|
resolution: str
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""Google Imagen aufrufen — synchron, liefert Base64-Bild direkt zurück.
|
"""Gemini Flash Image aufrufen — Text-to-Image und Image-to-Image.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prompt: Text-Prompt für die Bildgenerierung.
|
prompt: Text-Prompt.
|
||||||
image_base64: Optionales Referenzbild als Base64 (Image-to-Image).
|
image_base64: Optionales Referenzbild als Base64 (Image-to-Image).
|
||||||
aspect_ratio: Seitenverhältnis (z.B. "16:9").
|
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:
|
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:
|
if image_base64:
|
||||||
instance["rawBytes"] = image_base64
|
parts.append({
|
||||||
|
"inlineData": {
|
||||||
|
"mimeType": "image/jpeg",
|
||||||
|
"data": image_base64
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
body = json.dumps({
|
body = json.dumps({
|
||||||
"instances": [instance],
|
"contents": [{"parts": parts}],
|
||||||
"parameters": {
|
"generationConfig": {
|
||||||
"sampleCount": 1,
|
"responseModalities": ["image", "text"]
|
||||||
"aspectRatio": aspect_ratio,
|
|
||||||
"sampleImageSize": IMAGEN_SIZE_MAP.get(resolution, "2K"),
|
|
||||||
}
|
}
|
||||||
}).encode()
|
}).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("x-goog-api-key", key)
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=45) as resp:
|
||||||
return json.loads(resp.read())
|
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:
|
except urllib.error.HTTPError as e:
|
||||||
return {"error": f"HTTP {e.code}: {e.read().decode()[:300]}"}
|
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"})
|
self._json_reply(400, {"ok": False, "error": "Prompt fehlt"})
|
||||||
return
|
return
|
||||||
|
|
||||||
# Google Imagen aufrufen (synchron!)
|
# Gemini Flash Image aufrufen (synchron!)
|
||||||
result = _imagen_generate(prompt, image_base64, aspect_ratio, resolution)
|
result = _gemini_generate_image(prompt, image_base64, aspect_ratio, resolution)
|
||||||
if "error" in result:
|
if "error" in result:
|
||||||
self._json_reply(500, {"ok": False, "error": result["error"]})
|
self._json_reply(500, {"ok": False, "error": result["error"]})
|
||||||
return
|
return
|
||||||
|
|
||||||
# Base64-Bild extrahieren
|
# Base64-Bild (PNG) dekodieren
|
||||||
predictions = result.get("predictions") or []
|
b64_data = result.get("image_base64", "")
|
||||||
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:
|
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
|
return
|
||||||
|
|
||||||
img_bytes = b64.b64decode(b64_data)
|
img_bytes = b64.b64decode(b64_data)
|
||||||
|
|
||||||
# Im media-Ordner speichern (wie normale Uploads)
|
# Im media-Ordner speichern
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
month_dir = os.path.join(MEDIA_DIR, now.strftime("%Y"), now.strftime("%m"))
|
month_dir = os.path.join(MEDIA_DIR, now.strftime("%Y"), now.strftime("%m"))
|
||||||
os.makedirs(month_dir, exist_ok=True)
|
os.makedirs(month_dir, exist_ok=True)
|
||||||
filename = f"imagen_{now.strftime('%H%M%S')}.jpg"
|
base_name = f"gemini_{now.strftime('%H%M%S')}"
|
||||||
filepath = os.path.join(month_dir, filename)
|
ext = ".png" # Gemini liefert PNG
|
||||||
|
filepath = os.path.join(month_dir, base_name + ext)
|
||||||
|
|
||||||
with open(filepath, "wb") as f:
|
with open(filepath, "wb") as f:
|
||||||
f.write(img_bytes)
|
f.write(img_bytes)
|
||||||
|
|
||||||
# Optional: PIL-Normalisierung auf 1920x1080 @ 72 DPI
|
# Optional: PIL-Konvertierung zu JPEG (1920x1080 @ 72 DPI)
|
||||||
try:
|
try:
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
img = Image.open(filepath)
|
img = Image.open(filepath)
|
||||||
img = img.convert("RGB")
|
img = img.convert("RGB")
|
||||||
img = img.resize((1920, 1080), Image.LANCZOS)
|
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:
|
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, {
|
self._json_reply(200, {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"imageUrl": img_url,
|
"imageUrl": img_url,
|
||||||
|
|||||||
Reference in New Issue
Block a user