thats-me/dev/tools/lifeline-images/gemini.py
Kevin Adametz 10251e4f41 Doku und Werkzeuge: Tickets, Audit, Logos, Playwright-MCP
- dev/: Ticketliste, Audit und Vorschläge vom 17.09., Logo-SVGs,
  Lifeline-Bildwerkzeuge.
- AGENTS.md auf den aktuellen Stand (syncQueue statt syncService, kein
  legacy-Ordner mehr).
- .codex/ und __pycache__/ ignoriert; Playwright-MCP in .mcp.json.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-25 07:00:35 +00:00

67 lines
3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""Minimal Gemini image client (Nano Banana 2) – REST only, no SDK.
Reads GOOGLE_API_KEY from backend/.env. Usage as a module:
from gemini import generate
generate(prompt, refs=[path,...], out='x.png', aspect='4:3')
"""
import base64, json, os, re, sys, time, urllib.request, urllib.error
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))
MODEL = os.environ.get('LIFELINE_IMAGE_MODEL', 'gemini-3.1-flash-image')
def api_key():
for line in open(os.path.join(ROOT, 'backend', '.env')):
m = re.match(r'^GOOGLE_API_KEY=(.*)$', line.strip())
if m:
return m.group(1).strip().strip('"').strip("'")
sys.exit('GOOGLE_API_KEY missing in backend/.env')
def _mime(path):
with open(path, 'rb') as f:
head = f.read(8)
return 'image/png' if head.startswith(b'\x89PNG') else 'image/jpeg'
def generate(prompt, refs=(), out=None, aspect='4:3', size='1K', retries=3):
parts = []
for p in refs:
parts.append({'inline_data': {'mime_type': _mime(p), 'data': base64.b64encode(open(p, 'rb').read()).decode()}})
parts.append({'text': prompt})
body = {
'contents': [{'role': 'user', 'parts': parts}],
'generationConfig': {'responseModalities': ['IMAGE'], 'imageConfig': {'aspectRatio': aspect, 'imageSize': size}},
}
req = urllib.request.Request(
f'https://generativelanguage.googleapis.com/v1beta/models/{MODEL}:generateContent',
data=json.dumps(body).encode(), method='POST',
headers={'Content-Type': 'application/json', 'x-goog-api-key': api_key()},
)
for attempt in range(retries):
try:
with urllib.request.urlopen(req, timeout=180) as r:
res = json.load(r)
except urllib.error.HTTPError as e:
msg = e.read().decode()[:400]
if e.code in (429, 500, 503) and attempt < retries - 1:
time.sleep(10 * (attempt + 1)); continue
raise RuntimeError(f'HTTP {e.code}: {msg}')
cands = res.get('candidates') or []
for part in (cands[0].get('content', {}).get('parts', []) if cands else []):
data = part.get('inlineData') or part.get('inline_data')
if data:
raw = base64.b64decode(data['data'])
if out:
open(out, 'wb').write(raw)
return raw
reason = (cands[0].get('finishReason') if cands else None) or res.get('promptFeedback')
if attempt < retries - 1:
time.sleep(5); continue
raise RuntimeError(f'no image returned: {reason} {json.dumps(res)[:300]}')
if __name__ == '__main__':
import argparse
a = argparse.ArgumentParser()
a.add_argument('prompt'); a.add_argument('--ref', action='append', default=[]); a.add_argument('--out', required=True)
a.add_argument('--aspect', default='4:3'); a.add_argument('--size', default='1K')
ns = a.parse_args()
generate(ns.prompt, ns.ref, ns.out, ns.aspect, ns.size); print(ns.out)