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:
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user