feat: NanoBanana → Google Imagen Backend (synchron, kein Polling)
- Ersetzt NanoBanana (async, Credits-Probleme) durch Google Imagen - Synchron: Bild kommt als Base64 direkt in der Response - JS vereinfacht: pollStatus() komplett entfernt (-70 Zeilen) - Assign-Handler unterstützt jetzt lokale /media/ Pfade - Default-Auflösung 1K, Imagen-Key via bsn-secrets (google-imagen-key) - Editor: Header auf Google Imagen aktualisiert
This commit is contained in:
+106
-160
@@ -66,43 +66,56 @@ MONTHS = {
|
|||||||
"September": 9, "Oktober": 10, "November": 11, "Dezember": 12
|
"September": 9, "Oktober": 10, "November": 11, "Dezember": 12
|
||||||
}
|
}
|
||||||
|
|
||||||
# ── NanoBanana API Configuration ──────────────────────────────────────────
|
# ── Google Imagen API Configuration ────────────────────────────────────────
|
||||||
NANOBANANA_API_URL = "https://api.nanobananaapi.ai"
|
IMAGEN_API_URL = (
|
||||||
NANOBANANA_UPLOAD_DIR = "/tmp/nanobanana_uploads"
|
"https://generativelanguage.googleapis.com/v1beta/"
|
||||||
os.makedirs(NANOBANANA_UPLOAD_DIR, exist_ok=True)
|
"models/imagen-4.0-generate-001:predict"
|
||||||
|
)
|
||||||
|
IMAGEN_KEY_SECRET = "bsn/google-imagen-key"
|
||||||
|
|
||||||
def _get_nanobanana_key() -> str:
|
# Resolution Mapping (Imagen 4.0 verwendet 1K/2K direkt)
|
||||||
"""NanoBanana API-Key aus bsn-secrets lesen."""
|
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:
|
try:
|
||||||
r = subprocess.run(
|
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
|
capture_output=True, text=True, timeout=5
|
||||||
)
|
)
|
||||||
return r.stdout.strip()
|
return r.stdout.strip()
|
||||||
except Exception:
|
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."""
|
def _imagen_generate(
|
||||||
key = _get_nanobanana_key()
|
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:
|
if not key:
|
||||||
return {"error": "Kein API-Key"}
|
return {"error": "Kein Google Imagen API-Key"}
|
||||||
|
|
||||||
data = json.dumps({
|
body = json.dumps({
|
||||||
"prompt": prompt,
|
"instances": [{"prompt": prompt}],
|
||||||
"imageUrls": image_urls if image_urls else [],
|
"parameters": {
|
||||||
|
"sampleCount": 1,
|
||||||
"aspectRatio": aspect_ratio,
|
"aspectRatio": aspect_ratio,
|
||||||
"resolution": resolution,
|
"sampleImageSize": IMAGEN_SIZE_MAP.get(resolution, "2048x2048"),
|
||||||
"outputFormat": "jpg",
|
}
|
||||||
}).encode()
|
}).encode()
|
||||||
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(IMAGEN_API_URL, data=body, method="POST")
|
||||||
f"{NANOBANANA_API_URL}/api/v1/nanobanana/generate-2",
|
req.add_header("x-goog-api-key", key)
|
||||||
data=data, method="POST"
|
|
||||||
)
|
|
||||||
req.add_header("Authorization", "Bearer " + key)
|
|
||||||
req.add_header("Content-Type", "application/json")
|
req.add_header("Content-Type", "application/json")
|
||||||
req.add_header("User-Agent", "Mozilla/5.0") # Cloudflare-Workaround
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
@@ -110,24 +123,6 @@ def _nanobanana_generate(prompt: str, image_urls: list, aspect_ratio: str, resol
|
|||||||
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]}"}
|
||||||
|
|
||||||
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]:
|
def _get_unpublished_articles() -> list[dict]:
|
||||||
"""Liste aller unveröffentlichten Artikel (ohne joomla-id)."""
|
"""Liste aller unveröffentlichten Artikel (ohne joomla-id)."""
|
||||||
articles = []
|
articles = []
|
||||||
@@ -885,8 +880,6 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
self.wfile.write(html.encode("utf-8"))
|
self.wfile.write(html.encode("utf-8"))
|
||||||
elif path == "/nanobanana/articles":
|
elif path == "/nanobanana/articles":
|
||||||
self._handle_nanobanana_articles()
|
self._handle_nanobanana_articles()
|
||||||
elif path == "/nanobanana/status":
|
|
||||||
self._handle_nanobanana_status()
|
|
||||||
elif path.startswith("/media/"):
|
elif path.startswith("/media/"):
|
||||||
rel = path[len("/"):]
|
rel = path[len("/"):]
|
||||||
filepath = os.path.normpath(os.path.join(WORKSPACE, rel))
|
filepath = os.path.normpath(os.path.join(WORKSPACE, rel))
|
||||||
@@ -955,11 +948,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
elif path == "/nanobanana/generate":
|
elif path == "/nanobanana/generate":
|
||||||
if not self._check_token():
|
if not self._check_token():
|
||||||
return
|
return
|
||||||
self._handle_nanobanana_generate()
|
self._handle_image_generate()
|
||||||
elif path == "/nanobanana/status":
|
|
||||||
if not self._check_token():
|
|
||||||
return
|
|
||||||
self._handle_nanobanana_status()
|
|
||||||
elif path == "/nanobanana/articles":
|
elif path == "/nanobanana/articles":
|
||||||
if not self._check_token():
|
if not self._check_token():
|
||||||
return
|
return
|
||||||
@@ -981,11 +970,11 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
# NanoBanana Handler
|
# Imagen Handler (Google Imagen — synchron, kein Polling)
|
||||||
# ═══════════════════════════════════════════════════════════════════
|
# ═══════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
def _handle_nanobanana_generate(self):
|
def _handle_image_generate(self):
|
||||||
"""NanoBanana Bildgenerierung starten."""
|
"""Bildgenerierung via Google Imagen (synchron, Base64→JPEG)."""
|
||||||
content_type = self.headers.get("Content-Type", "")
|
content_type = self.headers.get("Content-Type", "")
|
||||||
if "multipart/form-data" not in content_type:
|
if "multipart/form-data" not in content_type:
|
||||||
self._json_reply(400, {"ok": False, "error": "Expected multipart/form-data"})
|
self._json_reply(400, {"ok": False, "error": "Expected multipart/form-data"})
|
||||||
@@ -1002,9 +991,8 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
# Multipart manuell parsen (Python 3.13 hat kein cgi)
|
# Multipart manuell parsen (Python 3.13 hat kein cgi)
|
||||||
parts = raw.split(b"--" + boundary.encode())
|
parts = raw.split(b"--" + boundary.encode())
|
||||||
prompt = ""
|
prompt = ""
|
||||||
image_urls = []
|
|
||||||
aspect_ratio = "16:9"
|
aspect_ratio = "16:9"
|
||||||
resolution = "2K"
|
resolution = "1K"
|
||||||
|
|
||||||
for part in parts:
|
for part in parts:
|
||||||
if b"Content-Disposition" not in part:
|
if b"Content-Disposition" not in part:
|
||||||
@@ -1013,8 +1001,7 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
if headers_end == -1:
|
if headers_end == -1:
|
||||||
continue
|
continue
|
||||||
headers_raw = part[:headers_end].decode("utf-8", errors="ignore")
|
headers_raw = part[:headers_end].decode("utf-8", errors="ignore")
|
||||||
content = part[headers_end + 4:]
|
content = part[headers_end + 4:].rstrip(b"\r\n")
|
||||||
content = content.rstrip(b"\r\n")
|
|
||||||
|
|
||||||
if 'name="prompt"' in headers_raw:
|
if 'name="prompt"' in headers_raw:
|
||||||
prompt = content.decode("utf-8", errors="ignore")
|
prompt = content.decode("utf-8", errors="ignore")
|
||||||
@@ -1022,62 +1009,56 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
aspect_ratio = content.decode("utf-8", errors="ignore")
|
aspect_ratio = content.decode("utf-8", errors="ignore")
|
||||||
elif 'name="resolution"' in headers_raw:
|
elif 'name="resolution"' in headers_raw:
|
||||||
resolution = content.decode("utf-8", errors="ignore")
|
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:
|
if not prompt:
|
||||||
self._json_reply(400, {"ok": False, "error": "Prompt fehlt"})
|
self._json_reply(400, {"ok": False, "error": "Prompt fehlt"})
|
||||||
return
|
return
|
||||||
|
|
||||||
result = _nanobanana_generate(prompt, image_urls, aspect_ratio, resolution)
|
# Google Imagen aufrufen (synchron!)
|
||||||
# Prüfe auf Transport-Fehler (Netzwerk/HTTP)
|
result = _imagen_generate(prompt, [], aspect_ratio, resolution)
|
||||||
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)
|
|
||||||
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
|
||||||
|
|
||||||
# Null-Safe: data kann None sein
|
# Base64-Bild extrahieren
|
||||||
data = result.get("data") or {}
|
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, {
|
self._json_reply(200, {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"successFlag": data.get("successFlag"),
|
"imageUrl": img_url,
|
||||||
"resultImageUrl": (data.get("response") or {}).get("resultImageUrl"),
|
"filepath": filepath,
|
||||||
"completeTime": data.get("completeTime"),
|
|
||||||
})
|
})
|
||||||
|
|
||||||
def _handle_nanobanana_articles(self):
|
def _handle_nanobanana_articles(self):
|
||||||
@@ -1107,8 +1088,17 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
|
|||||||
self._json_reply(400, {"ok": False, "error": "article und imageUrl erforderlich"})
|
self._json_reply(400, {"ok": False, "error": "article und imageUrl erforderlich"})
|
||||||
return
|
return
|
||||||
|
|
||||||
# Bild herunterladen
|
# Bild herunterladen oder lokal lesen
|
||||||
try:
|
try:
|
||||||
|
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 = urllib.request.Request(image_url)
|
||||||
req.add_header("User-Agent", "Mozilla/5.0")
|
req.add_header("User-Agent", "Mozilla/5.0")
|
||||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||||
@@ -1671,7 +1661,7 @@ NANOBANANA_HTML = """<!DOCTYPE html>
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>BSN Bild-Generator — NanoBanana</title>
|
<title>BSN Bild-Generator — Google Imagen</title>
|
||||||
<style>
|
<style>
|
||||||
:root {{
|
:root {{
|
||||||
--bsn-blue: #20228a;
|
--bsn-blue: #20228a;
|
||||||
@@ -1949,8 +1939,8 @@ textarea:focus, select:focus {{ border-color: var(--bsn-blue); outline: none; bo
|
|||||||
<div>
|
<div>
|
||||||
<label>🔍 Auflösung</label>
|
<label>🔍 Auflösung</label>
|
||||||
<select id="resolution">
|
<select id="resolution">
|
||||||
<option value="2K">2K — 1920×1080 (Standard)</option>
|
|
||||||
<option value="1K">1K — Schnell (geringere Qualität)</option>
|
<option value="1K">1K — Schnell (geringere Qualität)</option>
|
||||||
|
<option value="2K">2K — 1920×1080 (Standard)</option>
|
||||||
<option value="4K">4K — Höchste Qualität</option>
|
<option value="4K">4K — Höchste Qualität</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
@@ -1997,7 +1987,6 @@ const KEY = "{key}";
|
|||||||
const BASE = window.location.origin;
|
const BASE = window.location.origin;
|
||||||
let uploadedImageData = null; // File object
|
let uploadedImageData = null; // File object
|
||||||
let currentResultUrl = null;
|
let currentResultUrl = null;
|
||||||
let pollingTimer = null;
|
|
||||||
|
|
||||||
// ── Upload-Zone ──
|
// ── Upload-Zone ──
|
||||||
const dropZone = document.getElementById('dropZone');
|
const dropZone = document.getElementById('dropZone');
|
||||||
@@ -2057,11 +2046,11 @@ async function startGeneration() {{
|
|||||||
|
|
||||||
const btn = document.getElementById('startBtn');
|
const btn = document.getElementById('startBtn');
|
||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
btn.textContent = '⏳ Sende…';
|
btn.textContent = '⏳ Generiere…';
|
||||||
|
|
||||||
document.getElementById('resultBox').classList.remove('visible');
|
document.getElementById('resultBox').classList.remove('visible');
|
||||||
document.getElementById('spinner').classList.add('active');
|
document.getElementById('spinner').classList.add('active');
|
||||||
document.getElementById('spinnerText').textContent = 'Sende an NanoBanana…';
|
document.getElementById('spinnerText').textContent = 'Google Imagen generiert…';
|
||||||
|
|
||||||
try {{
|
try {{
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
@@ -2089,68 +2078,25 @@ async function startGeneration() {{
|
|||||||
throw new Error(data.error || 'Unbekannter Fehler (Status ' + resp.status + ')');
|
throw new Error(data.error || 'Unbekannter Fehler (Status ' + resp.status + ')');
|
||||||
}}
|
}}
|
||||||
|
|
||||||
document.getElementById('spinnerText').textContent = 'Warte auf Ergebnis…';
|
// DIREKT Ergebnis anzeigen — Google Imagen ist synchron!
|
||||||
pollStatus(data.taskId);
|
document.getElementById('spinner').classList.remove('active');
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = '🚀 Bild generieren';
|
||||||
|
|
||||||
|
currentResultUrl = data.imageUrl;
|
||||||
|
document.getElementById('resultImg').src = currentResultUrl;
|
||||||
|
document.getElementById('resultBox').classList.add('visible');
|
||||||
|
showToast('✅ Bild fertig!', 'success');
|
||||||
|
loadArticles();
|
||||||
|
|
||||||
}} catch (err) {{
|
}} catch (err) {{
|
||||||
document.getElementById('spinner').classList.remove('active');
|
document.getElementById('spinner').classList.remove('active');
|
||||||
btn.disabled = false;
|
btn.disabled = false;
|
||||||
btn.textContent = '🚀 Bild generieren';
|
btn.textContent = '🚀 Bild generieren';
|
||||||
showToast('Fehler: ' + err.message, 'error');
|
showToast('❌ Fehler: ' + err.message, 'error');
|
||||||
}}
|
}}
|
||||||
}}
|
}}
|
||||||
|
|
||||||
async function pollStatus(taskId) {{
|
|
||||||
let attempts = 0;
|
|
||||||
|
|
||||||
const poll = async () => {{
|
|
||||||
attempts++;
|
|
||||||
document.getElementById('spinnerText').textContent = 'Generiere Bild… (' + (attempts * 5) + 's)';
|
|
||||||
|
|
||||||
try {{
|
|
||||||
const resp = await fetch(BASE + '/nanobanana/status?key=' + KEY + '&taskId=' + taskId);
|
|
||||||
const data = await resp.json();
|
|
||||||
|
|
||||||
if (data.successFlag === 1) {{
|
|
||||||
// Fertig!
|
|
||||||
document.getElementById('spinner').classList.remove('active');
|
|
||||||
document.getElementById('startBtn').disabled = false;
|
|
||||||
document.getElementById('startBtn').textContent = '🚀 Bild generieren';
|
|
||||||
|
|
||||||
currentResultUrl = data.resultImageUrl;
|
|
||||||
document.getElementById('resultImg').src = currentResultUrl;
|
|
||||||
document.getElementById('resultBox').classList.add('visible');
|
|
||||||
showToast('Bild fertig!', 'success');
|
|
||||||
|
|
||||||
// Artikel-Dropdown laden
|
|
||||||
loadArticles();
|
|
||||||
return;
|
|
||||||
}}
|
|
||||||
|
|
||||||
if (data.successFlag === 2) {{
|
|
||||||
throw new Error('Generation fehlgeschlagen');
|
|
||||||
}}
|
|
||||||
|
|
||||||
}} catch (err) {{
|
|
||||||
if (err.message === 'Generation fehlgeschlagen') throw err;
|
|
||||||
// Netzwerkfehler ignorieren, weiter pollen
|
|
||||||
}}
|
|
||||||
|
|
||||||
if (attempts < 60) {{
|
|
||||||
pollingTimer = setTimeout(poll, 5000);
|
|
||||||
}} else {{
|
|
||||||
throw new Error('Timeout nach 5 Minuten');
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
|
|
||||||
poll().catch(err => {{
|
|
||||||
document.getElementById('spinner').classList.remove('active');
|
|
||||||
document.getElementById('startBtn').disabled = false;
|
|
||||||
document.getElementById('startBtn').textContent = '🚀 Bild generieren';
|
|
||||||
showToast('Fehler: ' + err.message, 'error');
|
|
||||||
}});
|
|
||||||
}}
|
|
||||||
|
|
||||||
// ── Artikel laden ──
|
// ── Artikel laden ──
|
||||||
async function loadArticles() {{
|
async function loadArticles() {{
|
||||||
try {{
|
try {{
|
||||||
|
|||||||
Reference in New Issue
Block a user