feat(flux): Bildgenerierung via FLUX.1-dev — flux-bridge auf Gamebox

Eigener Compose-Stack im /flux Verzeichnis (kann auf separater Maschine
laufen). aria-bridge routet flux_request via RVS, ARIA referenziert das
fertige PNG im Reply mit [FILE: ...]-Marker. Brain-Tool flux_generate
mit Caps fuer steps/dimension.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-16 22:33:48 +02:00
parent 33d5be781f
commit 7e53dcfed3
12 changed files with 984 additions and 2 deletions
+102
View File
@@ -18,6 +18,9 @@ from __future__ import annotations
import json
import logging
import os
import urllib.error
import urllib.request
from typing import Optional
from conversation import Conversation, Turn
@@ -28,6 +31,12 @@ import skills as skills_mod
import triggers as triggers_mod
import watcher as watcher_mod
BRIDGE_URL = os.environ.get("BRIDGE_URL", "http://aria-bridge:8090")
# FLUX-Render kann bis ~90s dauern, beim ersten Render nach Container-Start
# laedt die flux-bridge zudem ~24 GB Modell von HF (~5-10 min). Brain wartet
# synchron — Stefan kuendigt es vorher an wenn er weiss dass es feuert.
FLUX_HTTP_TIMEOUT_SEC = 1200
logger = logging.getLogger(__name__)
@@ -215,6 +224,47 @@ META_TOOLS = [
},
},
},
{
"type": "function",
"function": {
"name": "flux_generate",
"description": (
"Generiere ein Bild aus einem Text-Prompt via FLUX.1-dev auf der Gamebox-"
"GPU. Brauchbar fuer 'mal mir ein X', 'wie sieht ein Y aus?', "
"Mockups, Konzept-Skizzen. Render dauert 20-90s — Stefan kuendigt "
"es an wenn er weiss dass es laeuft.\n\n"
"**Schreibe deine Antwort wie immer auf Deutsch**, und referenziere das "
"fertige Bild MIT dem `[FILE: ...]`-Marker, GENAU im Pfad-Format das das "
"Tool zurueckgibt. Beispiel:\n"
" 'Hier dein Aquarell:\\n[FILE: /shared/uploads/aria_generated_1234.png]'\n\n"
"Der Marker wird beim App-Renderer ausgeblendet und das Bild stattdessen "
"inline als Anhang gezeigt.\n\n"
"**Prompt-Sprache: bevorzugt Englisch.** FLUX versteht zwar Deutsch, "
"liefert aber mit englischen Prompts deutlich konsistentere Ergebnisse. "
"Uebersetze Stefans deutsche Beschreibung selbststaendig.\n\n"
"Caps:\n"
"- `width`/`height`: 256-1536, wird auf Vielfache von 64 gesnappt (Default 1024)\n"
"- `steps`: 1-50 (Default 28 fuer FLUX.1-dev, 4 fuer schnell)\n"
"- `guidance_scale`: 0.0-20.0 (Default 3.5)\n"
"- `seed`: optional, gleicher seed + gleicher prompt → gleiches Bild"
),
"parameters": {
"type": "object",
"properties": {
"prompt": {
"type": "string",
"description": "Englischer Bild-Prompt. So konkret wie moeglich (Motiv, Stil, Licht, Kamera).",
},
"width": {"type": "integer", "description": "Breite in px (Default 1024, max 1536)"},
"height": {"type": "integer", "description": "Hoehe in px (Default 1024, max 1536)"},
"steps": {"type": "integer", "description": "Inference-Steps (Default 28, max 50). Mehr = besser+langsamer."},
"guidance_scale": {"type": "number", "description": "Wie strikt am Prompt kleben (Default 3.5)"},
"seed": {"type": "integer", "description": "Reproduzierbarkeits-Seed (optional)"},
},
"required": ["prompt"],
},
},
},
{
"type": "function",
"function": {
@@ -607,6 +657,58 @@ class Agent:
else:
lines.append(f"- {t['name']} ({t['type']}, {state})")
return "\n".join(lines)
if name == "flux_generate":
prompt = (arguments.get("prompt") or "").strip()
if not prompt:
return "FEHLER: prompt ist Pflicht."
req: dict = {"prompt": prompt}
for key in ("width", "height", "steps", "seed"):
if key in arguments and arguments[key] is not None:
try:
req[key] = int(arguments[key])
except (TypeError, ValueError):
pass
if arguments.get("guidance_scale") is not None:
try:
req["guidance_scale"] = float(arguments["guidance_scale"])
except (TypeError, ValueError):
pass
try:
body = json.dumps(req).encode("utf-8")
http_req = urllib.request.Request(
f"{BRIDGE_URL}/internal/flux-generate", data=body, method="POST",
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(http_req, timeout=FLUX_HTTP_TIMEOUT_SEC) as resp:
raw = resp.read()
result = json.loads(raw.decode("utf-8", "ignore"))
except urllib.error.HTTPError as exc:
try:
err_body = exc.read().decode("utf-8", "ignore")
err_data = json.loads(err_body)
err = err_data.get("error") or err_body
except Exception:
err = str(exc)
return f"FEHLER (flux-bridge): {err}"
except Exception as exc:
logger.exception("flux_generate HTTP-Call fehlgeschlagen")
return f"FEHLER: flux-bridge nicht erreichbar ({exc})"
if not result.get("ok"):
return f"FEHLER (flux-bridge): {result.get('error', 'unbekannt')}"
# Kompakte Rueckmeldung: Pfad + Render-Stats. Brain bettet den
# Pfad in ihre Antwort als [FILE: ...]-Marker ein (siehe Tool-Beschreibung).
return (
f"OK — Bild generiert.\n"
f"path: {result['path']}\n"
f"size: {result.get('width','?')}x{result.get('height','?')} "
f"({result.get('sizeBytes',0)//1024} KB)\n"
f"steps={result.get('steps','?')} guidance={result.get('guidance','?')} "
f"seed={result.get('seed','?')} model={result.get('model','?')}\n"
f"renderSeconds={result.get('renderSeconds','?')}\n\n"
f"WICHTIG: Schreibe in deiner Antwort an Stefan den Pfad EXAKT als "
f"Marker: [FILE: {result['path']}] — dann zeigt die App das Bild inline."
)
if name == "memory_search":
query = (arguments.get("query") or "").strip()
if not query: