Integrate Qwen image prompt enhancers

This commit is contained in:
Mikei386
2026-09-21 14:51:29 +02:00
parent b832157226
commit 5106d0d2ed
5 changed files with 403 additions and 10 deletions
+182 -5
View File
@@ -146,6 +146,18 @@ IMAGE_PYTHON = os.environ.get(
"IMAGE_PYTHON", "/opt/mike-ai/ai-profile-router/venv/bin/python")
IMAGE_WORKER_URL = os.environ.get("IMAGE_WORKER_URL", "").rstrip("/")
IMAGE_WORKER_TOKEN = os.environ.get("IMAGE_WORKER_TOKEN", "").strip()
IMAGE_PROMPT_I2I_URL = os.environ.get(
"IMAGE_PROMPT_I2I_URL", "").rstrip("/")
IMAGE_PROMPT_T2I_URL = os.environ.get(
"IMAGE_PROMPT_T2I_URL", "").rstrip("/")
IMAGE_PROMPT_I2I_SYSTEM_FILE = os.environ.get(
"IMAGE_PROMPT_I2I_SYSTEM_FILE",
"/etc/mike-ai/qwen-image-pe-i2i-system-prompt.txt")
IMAGE_PROMPT_T2I_SYSTEM_FILE = os.environ.get(
"IMAGE_PROMPT_T2I_SYSTEM_FILE",
"/etc/mike-ai/qwen-image-pe-t2i-system-prompt.txt")
IMAGE_PROMPT_ENHANCER_TIMEOUT = float(os.environ.get(
"IMAGE_PROMPT_ENHANCER_TIMEOUT", "180"))
IMAGE_MODEL_NAME = os.environ.get(
"IMAGE_MODEL_NAME", "Qwen-Image-2.1-int8")
IMAGE_INFERENCE_STEPS = int(os.environ.get("IMAGE_INFERENCE_STEPS", "25"))
@@ -182,6 +194,16 @@ IMAGE_QUALITY = {"standard": IMAGE_INFERENCE_STEPS,
IMAGE_DEFAULT_QUALITY = "standard"
IMAGE_MAX_N = 4
IMAGE_RATIO_SIZES = {
"1:1": (1024, 1024),
"3:2": (1536, 1024),
"2:3": (1024, 1536),
"4:3": (1536, 1024),
"3:4": (1024, 1536),
"16:9": (1920, 1088),
"9:16": (1088, 1920),
}
# --- Sprachausgabe (Qwen3-TTS über das interne Normalisierungs-Gateway) ---
TTS_WORKER_URL = os.environ.get("TTS_WORKER_URL", "http://127.0.0.1:8085")
TTS_TIMEOUT = float(os.environ.get("TTS_TIMEOUT", "300")) # s, pro Synthese
@@ -1386,11 +1408,146 @@ def _restore_qwen(profile: str) -> None:
RUNTIME.save(last_profile=profile, phase="idle")
def _image_data_url(path: str) -> str:
"""Read one already validated local reference image as a data URL."""
with open(path, "rb") as handle:
data = handle.read()
if data.startswith(b"\x89PNG\r\n\x1a\n"):
mime = "image/png"
elif data.startswith(b"\xff\xd8\xff"):
mime = "image/jpeg"
elif data.startswith(b"RIFF") and data[8:12] == b"WEBP":
mime = "image/webp"
else:
raise RuntimeError(f"Referenzbild hat ein unbekanntes Format: {path}")
return f"data:{mime};base64,{base64.b64encode(data).decode()}"
def _parse_prompt_enhancer_result(content: object) -> dict:
if not isinstance(content, str) or not content.strip():
raise RuntimeError("Prompt-Enhancer lieferte keine Antwort")
text = content.strip()
if text.startswith("```"):
text = re.sub(r"^```(?:json)?\s*", "", text, flags=re.IGNORECASE)
text = re.sub(r"\s*```$", "", text)
try:
result = json.loads(text)
except ValueError:
start, end = text.find("{"), text.rfind("}")
if start < 0 or end <= start:
raise RuntimeError("Prompt-Enhancer lieferte kein JSON")
try:
result = json.loads(text[start:end + 1])
except ValueError as exc:
raise RuntimeError("Prompt-Enhancer lieferte ungültiges JSON") from exc
if not isinstance(result, dict):
raise RuntimeError("Prompt-Enhancer lieferte kein JSON-Objekt")
rewritten = result.get("rewritten_prompt")
if not isinstance(rewritten, str) or not rewritten.strip():
raise RuntimeError("Prompt-Enhancer lieferte keinen rewritten_prompt")
if len(rewritten) > 8000:
raise RuntimeError("Aufbereiteter Bildprompt ist länger als 8000 Zeichen")
result["rewritten_prompt"] = rewritten.strip()
return result
def _enhance_image_prompt(prompt: str, source_files: list[str]) -> tuple[str, dict]:
"""Run the official Qwen Image 2.1 prompt enhancer on the RTX 3060."""
editing = bool(source_files)
kind = "image-prompt-i2i" if editing else "image-prompt-t2i"
url = IMAGE_PROMPT_I2I_URL if editing else IMAGE_PROMPT_T2I_URL
system_file = (IMAGE_PROMPT_I2I_SYSTEM_FILE if editing
else IMAGE_PROMPT_T2I_SYSTEM_FILE)
model = "qwen-image-pe-i2i" if editing else "qwen-image-pe-t2i"
if not PROFILE_CONTROL_URL or not url:
raise RuntimeError("Qwen-Image-Prompt-Enhancer ist nicht konfiguriert")
try:
with open(system_file, encoding="utf-8") as handle:
system_prompt = handle.read().strip()
except OSError as exc:
raise RuntimeError(f"Systemprompt des Prompt-Enhancers fehlt: {exc}") from exc
started = time.monotonic()
_profile_controller_request("POST", f"/workers/{kind}/start")
try:
deadline = time.monotonic() + IMAGE_START_TIMEOUT
while True:
try:
with urllib.request.urlopen(url + "/health", timeout=3) as response:
health = json.load(response)
if health.get("status") == "ok":
break
except (OSError, urllib.error.URLError, TimeoutError, ValueError):
pass
if time.monotonic() >= deadline:
raise RuntimeError("Prompt-Enhancer hat nicht gestartet")
time.sleep(1)
if editing:
user_content: object = [
{"type": "image_url", "image_url": {"url": _image_data_url(path)}}
for path in source_files
]
user_content.append({"type": "text", "text": prompt})
else:
user_content = prompt
request_body = {
"model": model,
"messages": [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_content},
],
"temperature": 1.0,
"top_p": 0.95,
"top_k": 20,
"presence_penalty": 0.0 if editing else 1.5,
"max_tokens": 4096,
"thinking_budget_tokens": 2048,
"chat_template_kwargs": {
"enable_thinking": True,
"reasoning_effort": "low",
},
}
req = urllib.request.Request(
url + "/v1/chat/completions",
data=json.dumps(request_body).encode(),
method="POST",
headers={"Content-Type": "application/json"},
)
try:
with urllib.request.urlopen(
req, timeout=IMAGE_PROMPT_ENHANCER_TIMEOUT) as response:
payload = json.load(response)
except urllib.error.HTTPError as exc:
detail = exc.read(4096).decode(errors="replace")
raise RuntimeError(
f"Prompt-Enhancer HTTP {exc.code}: {detail[-500:]}") from exc
try:
content = payload["choices"][0]["message"]["content"]
except (KeyError, IndexError, TypeError) as exc:
raise RuntimeError("Prompt-Enhancer-Antwort ist unvollständig") from exc
result = _parse_prompt_enhancer_result(content)
metadata = {
"model": model,
"quantization": "Q5_K_M",
"seconds": round(time.monotonic() - started, 3),
"wh_ratio": result.get("wh_ratio"),
"ratio_follow": result.get("ratio_follow"),
}
return result["rewritten_prompt"], metadata
finally:
try:
_profile_controller_request("POST", f"/workers/{kind}/stop")
except Exception as exc:
log.warning("Prompt-Enhancer ließ sich nicht stoppen: %s", exc)
def generate_image(prompt: str, width: int, height: int, steps: int,
guidance: float, seed: int | None, n: int,
quality: str = "standard",
source_files: list[str] | None = None,
model: str = IMAGE_MODEL_NAME,
size_explicit: bool = False,
) -> tuple[list[str], str | None]:
"""Orchestriert die Bildgenerierung inkl. Qwen-Hotswap.
@@ -1411,6 +1568,8 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
warning: str | None = None
img.last_error = None
img.current_model = model
original_prompt = prompt
prompt_enhancer: dict | None = None
# Qwen wird gestoppt → für Chats nicht verfügbar (die warten).
_set_qwen_unavailable(True)
try:
@@ -1433,11 +1592,26 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
f"(Exit {proc.returncode}): {out[-500:]}")
_wait_upstream_down(time.monotonic() + 60)
# 2) Worker starten (Modell wird beim ersten generate geladen).
# 2) Den knappen Benutzerprompt mit dem offiziellen Qwen-
# Prompt-Enhancer auf der RTX 3060 in einen belastbaren
# Produktionsprompt umschreiben. Danach wird der Enhancer wieder
# beendet, bevor der eigentliche Bildworker startet.
img.phase = "enhancing-prompt"
prompt, prompt_enhancer = _enhance_image_prompt(
original_prompt, source_files or [])
ratio = prompt_enhancer.get("wh_ratio")
if not size_explicit and isinstance(ratio, str):
width, height = IMAGE_RATIO_SIZES.get(
ratio.strip(), (width, height))
log.info("Bildprompt aufbereitet (%s, %.1f s, Verhältnis %s)",
prompt_enhancer["model"],
prompt_enhancer["seconds"], ratio)
# 3) Worker starten (Modell wird beim ersten generate geladen).
img.phase = "loading-image"
worker = _worker()
# 3) Generieren.
# 4) Generieren.
for i in range(n):
img.phase = "generating"
filename = time.strftime("%Y%m%d-%H%M%S") + \
@@ -1465,6 +1639,8 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
# Metadaten speichern (Sidecar-JSON).
meta = {
"prompt": prompt,
"original_prompt": original_prompt,
"prompt_enhancer": prompt_enhancer,
"seed": seed,
"width": width,
"height": height,
@@ -1493,7 +1669,7 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
if removed:
log.info("Bild-Retention: %d alte Bilder entfernt", len(removed))
# 4) Worker vollständig beenden (VRAM + CUDA-Kontext freigeben).
# 5) Worker vollständig beenden (VRAM + CUDA-Kontext freigeben).
img.phase = "unloading-image"
worker.stop()
img.worker = None
@@ -1510,7 +1686,7 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
img.worker = None
raise
finally:
# 5) Qwen immer wiederherstellen.
# 6) Qwen immer wiederherstellen.
img.phase = "restoring-qwen"
try:
_restore_qwen(profile)
@@ -2358,6 +2534,7 @@ class Handler(BaseHTTPRequestHandler):
return
# Größe
size_explicit = "size" in data
size = data.get("size", "1024x1024")
if size not in IMAGE_SIZES:
self._send_error(
@@ -2425,7 +2602,7 @@ class Handler(BaseHTTPRequestHandler):
try:
results, warning = generate_image(
prompt.strip(), width, height, steps, guidance, seed, n,
quality, source_files, model)
quality, source_files, model, size_explicit)
except (ValueError, RuntimeError) as e:
self._send_error(503, str(e), "server_error", "image_generation_failed")
return