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
+146
View File
@@ -572,6 +572,8 @@ services:
ALLOWED_PROFILES: fast,medium,large,ultra,uncensored ALLOWED_PROFILES: fast,medium,large,ultra,uncensored
IMAGE_WORKER: image IMAGE_WORKER: image
RESTORE_WORKER: restore RESTORE_WORKER: restore
IMAGE_PROMPT_I2I_WORKER: image-prompt-i2i
IMAGE_PROMPT_T2I_WORKER: image-prompt-t2i
FLUX_STANDBY_WORKER: flux-standby FLUX_STANDBY_WORKER: flux-standby
TTS_WORKER: qwen3 TTS_WORKER: qwen3
MUSIC_WORKER: acestep MUSIC_WORKER: acestep
@@ -602,6 +604,8 @@ services:
volumes: volumes:
- ./router/router_profiles.json:/etc/mike-ai/router-profiles.json:ro - ./router/router_profiles.json:/etc/mike-ai/router-profiles.json:ro
- ./config/global-system-policy.txt:/etc/mike-ai/global-system-policy.txt:ro - ./config/global-system-policy.txt:/etc/mike-ai/global-system-policy.txt:ro
- "${QWEN_IMAGE_PE_I2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-i2i-q5}/system_prompt.txt:/etc/mike-ai/qwen-image-pe-i2i-system-prompt.txt:ro"
- "${QWEN_IMAGE_PE_T2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-t2i-q5}/system_prompt.txt:/etc/mike-ai/qwen-image-pe-t2i-system-prompt.txt:ro"
- router-state:/var/lib/mike-ai-profile-router - router-state:/var/lib/mike-ai-profile-router
- router-images:/data/images - router-images:/data/images
environment: environment:
@@ -631,6 +635,11 @@ services:
IMAGE_DIR: /data/images IMAGE_DIR: /data/images
IMAGE_WORKER_URL: http://image-worker:8086 IMAGE_WORKER_URL: http://image-worker:8086
IMAGE_WORKER_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}" IMAGE_WORKER_TOKEN: "${CONTROLLER_TOKEN:?CONTROLLER_TOKEN is required}"
IMAGE_PROMPT_I2I_URL: http://image-prompt-enhancer-i2i:8080
IMAGE_PROMPT_T2I_URL: http://image-prompt-enhancer-t2i:8080
IMAGE_PROMPT_I2I_SYSTEM_FILE: /etc/mike-ai/qwen-image-pe-i2i-system-prompt.txt
IMAGE_PROMPT_T2I_SYSTEM_FILE: /etc/mike-ai/qwen-image-pe-t2i-system-prompt.txt
IMAGE_PROMPT_ENHANCER_TIMEOUT: "180"
IMAGE_MODEL_NAME: Qwen-Image-2.1-int8 IMAGE_MODEL_NAME: Qwen-Image-2.1-int8
IMAGE_INFERENCE_STEPS: "25" IMAGE_INFERENCE_STEPS: "25"
CHAT_IMAGE_ALLOW_REMOTE_URLS: "false" CHAT_IMAGE_ALLOW_REMOTE_URLS: "false"
@@ -716,6 +725,143 @@ services:
retries: 90 retries: 90
start_period: 10s start_period: 10s
image-prompt-enhancer-i2i:
image: mike-ai/llama.cpp:b10930
container_name: mike-ai-image-prompt-enhancer-i2i
restart: "no"
profiles: [image]
labels:
com.mike-ai.image-worker: image-prompt-i2i
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["${QWEN_IMAGE_PE_GPU:-0}"]
capabilities: [gpu]
read_only: true
tmpfs: ["/tmp:size=512m,mode=1777"]
volumes:
- "${QWEN_IMAGE_PE_I2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-i2i-q5}:/models:ro"
command:
- --model
- /models/Qwen-Image-2.1-PE-I2I.Q5_K_M.gguf
- --mmproj
- /models/Qwen-Image-2.1-PE-I2I.mmproj-bf16.gguf
- --mmproj-offload
- --mmproj-device
- CUDA0
- --alias
- qwen-image-pe-i2i
- --ctx-size
- "16384"
- --flash-attn
- "on"
- --cache-type-k
- q8_0
- --cache-type-v
- q8_0
- --threads
- "6"
- --threads-batch
- "6"
- --batch-size
- "1024"
- --ubatch-size
- "128"
- --parallel
- "1"
- --jinja
- --reasoning
- auto
- --host
- 0.0.0.0
- --port
- "8080"
- --metrics
- --n-gpu-layers
- all
- --device
- CUDA0
- --split-mode
- none
- --no-ui
networks: [inference]
security_opt: ["no-new-privileges:true"]
cap_drop: [ALL]
healthcheck:
test: [CMD, curl, -fsS, "http://127.0.0.1:8080/health"]
interval: 5s
timeout: 3s
retries: 40
start_period: 10s
image-prompt-enhancer-t2i:
image: mike-ai/llama.cpp:b10930
container_name: mike-ai-image-prompt-enhancer-t2i
restart: "no"
profiles: [image]
labels:
com.mike-ai.image-worker: image-prompt-t2i
deploy:
resources:
reservations:
devices:
- driver: nvidia
device_ids: ["${QWEN_IMAGE_PE_GPU:-0}"]
capabilities: [gpu]
read_only: true
tmpfs: ["/tmp:size=512m,mode=1777"]
volumes:
- "${QWEN_IMAGE_PE_T2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-t2i-q5}:/models:ro"
command:
- --model
- /models/Qwen-Image-2.1-PE-T2I.Q5_K_M.gguf
- --alias
- qwen-image-pe-t2i
- --ctx-size
- "16384"
- --flash-attn
- "on"
- --cache-type-k
- q8_0
- --cache-type-v
- q8_0
- --threads
- "6"
- --threads-batch
- "6"
- --batch-size
- "1024"
- --ubatch-size
- "128"
- --parallel
- "1"
- --jinja
- --reasoning
- auto
- --host
- 0.0.0.0
- --port
- "8080"
- --metrics
- --n-gpu-layers
- all
- --device
- CUDA0
- --split-mode
- none
- --no-ui
networks: [inference]
security_opt: ["no-new-privileges:true"]
cap_drop: [ALL]
healthcheck:
test: [CMD, curl, -fsS, "http://127.0.0.1:8080/health"]
interval: 5s
timeout: 3s
retries: 40
start_period: 10s
# Previous production image model, retained as a stopped rollback target. # Previous production image model, retained as a stopped rollback target.
# It is outside the normal router path and can only be started through the # It is outside the normal router path and can only be started through the
# controller's allowlisted flux-standby endpoint. # controller's allowlisted flux-standby endpoint.
+12
View File
@@ -13,6 +13,18 @@ import ai_profile_router as router
class CoordinationTests(unittest.TestCase): class CoordinationTests(unittest.TestCase):
def test_prompt_enhancer_accepts_plain_and_fenced_json(self):
plain = router._parse_prompt_enhancer_result(
'{"rewritten_prompt":"new scene","wh_ratio":"3:2"}')
fenced = router._parse_prompt_enhancer_result(
'```json\n{"rewritten_prompt":"portrait","wh_ratio":"3:4"}\n```')
self.assertEqual(plain['rewritten_prompt'], 'new scene')
self.assertEqual(fenced['wh_ratio'], '3:4')
def test_prompt_enhancer_rejects_missing_rewrite(self):
with self.assertRaisesRegex(RuntimeError, 'rewritten_prompt'):
router._parse_prompt_enhancer_result('{"wh_ratio":"1:1"}')
def test_mode_uses_one_consistent_controller_snapshot(self): def test_mode_uses_one_consistent_controller_snapshot(self):
with patch.object(router, 'PROFILE_CONTROL_URL', 'http://controller'), \ with patch.object(router, 'PROFILE_CONTROL_URL', 'http://controller'), \
patch.object(router, '_profile_controller_request', return_value={ patch.object(router, '_profile_controller_request', return_value={
@@ -27,6 +27,10 @@ LABEL_KEY = "com.mike-ai.llama-profile"
IMAGE_LABEL_KEY = "com.mike-ai.image-worker" IMAGE_LABEL_KEY = "com.mike-ai.image-worker"
IMAGE_WORKER = os.environ.get("IMAGE_WORKER", "image") IMAGE_WORKER = os.environ.get("IMAGE_WORKER", "image")
RESTORE_WORKER = os.environ.get("RESTORE_WORKER", "restore") RESTORE_WORKER = os.environ.get("RESTORE_WORKER", "restore")
IMAGE_PROMPT_I2I_WORKER = os.environ.get(
"IMAGE_PROMPT_I2I_WORKER", "image-prompt-i2i").strip()
IMAGE_PROMPT_T2I_WORKER = os.environ.get(
"IMAGE_PROMPT_T2I_WORKER", "image-prompt-t2i").strip()
FLUX_STANDBY_WORKER = os.environ.get( FLUX_STANDBY_WORKER = os.environ.get(
"FLUX_STANDBY_WORKER", "flux-standby").strip() "FLUX_STANDBY_WORKER", "flux-standby").strip()
TTS_LABEL_KEY = "com.mike-ai.tts-worker" TTS_LABEL_KEY = "com.mike-ai.tts-worker"
@@ -102,7 +106,12 @@ def image_container(kind: str = IMAGE_WORKER) -> dict:
def image_containers() -> list[dict]: def image_containers() -> list[dict]:
"""All allowlisted GPU workers that must never overlap an LLM.""" """All allowlisted GPU workers that must never overlap an LLM."""
allowed = {IMAGE_WORKER, RESTORE_WORKER} allowed = {
IMAGE_WORKER,
RESTORE_WORKER,
IMAGE_PROMPT_I2I_WORKER,
IMAGE_PROMPT_T2I_WORKER,
}
if FLUX_STANDBY_WORKER: if FLUX_STANDBY_WORKER:
allowed.add(FLUX_STANDBY_WORKER) allowed.add(FLUX_STANDBY_WORKER)
return [item for item in labelled_containers(IMAGE_LABEL_KEY) return [item for item in labelled_containers(IMAGE_LABEL_KEY)
@@ -363,7 +372,13 @@ def stop_inference() -> dict:
def set_image_worker(running: bool, kind: str = IMAGE_WORKER) -> dict: def set_image_worker(running: bool, kind: str = IMAGE_WORKER) -> dict:
if kind not in {IMAGE_WORKER, RESTORE_WORKER, FLUX_STANDBY_WORKER}: if kind not in {
IMAGE_WORKER,
RESTORE_WORKER,
FLUX_STANDBY_WORKER,
IMAGE_PROMPT_I2I_WORKER,
IMAGE_PROMPT_T2I_WORKER,
}:
raise ValueError("worker is not allowlisted") raise ValueError("worker is not allowlisted")
with LOCK: with LOCK:
item = image_container(kind) item = image_container(kind)
@@ -799,6 +814,10 @@ class Handler(BaseHTTPRequestHandler):
worker_paths = { worker_paths = {
"/workers/image/start": (IMAGE_WORKER, True), "/workers/image/start": (IMAGE_WORKER, True),
"/workers/image/stop": (IMAGE_WORKER, False), "/workers/image/stop": (IMAGE_WORKER, False),
"/workers/image-prompt-i2i/start": (IMAGE_PROMPT_I2I_WORKER, True),
"/workers/image-prompt-i2i/stop": (IMAGE_PROMPT_I2I_WORKER, False),
"/workers/image-prompt-t2i/start": (IMAGE_PROMPT_T2I_WORKER, True),
"/workers/image-prompt-t2i/stop": (IMAGE_PROMPT_T2I_WORKER, False),
"/workers/restore/start": (RESTORE_WORKER, True), "/workers/restore/start": (RESTORE_WORKER, True),
"/workers/restore/stop": (RESTORE_WORKER, False), "/workers/restore/stop": (RESTORE_WORKER, False),
"/workers/flux-standby/start": (FLUX_STANDBY_WORKER, True), "/workers/flux-standby/start": (FLUX_STANDBY_WORKER, True),
+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_PYTHON", "/opt/mike-ai/ai-profile-router/venv/bin/python")
IMAGE_WORKER_URL = os.environ.get("IMAGE_WORKER_URL", "").rstrip("/") IMAGE_WORKER_URL = os.environ.get("IMAGE_WORKER_URL", "").rstrip("/")
IMAGE_WORKER_TOKEN = os.environ.get("IMAGE_WORKER_TOKEN", "").strip() 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 = os.environ.get(
"IMAGE_MODEL_NAME", "Qwen-Image-2.1-int8") "IMAGE_MODEL_NAME", "Qwen-Image-2.1-int8")
IMAGE_INFERENCE_STEPS = int(os.environ.get("IMAGE_INFERENCE_STEPS", "25")) 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_DEFAULT_QUALITY = "standard"
IMAGE_MAX_N = 4 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) --- # --- Sprachausgabe (Qwen3-TTS über das interne Normalisierungs-Gateway) ---
TTS_WORKER_URL = os.environ.get("TTS_WORKER_URL", "http://127.0.0.1:8085") 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 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") 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, def generate_image(prompt: str, width: int, height: int, steps: int,
guidance: float, seed: int | None, n: int, guidance: float, seed: int | None, n: int,
quality: str = "standard", quality: str = "standard",
source_files: list[str] | None = None, source_files: list[str] | None = None,
model: str = IMAGE_MODEL_NAME, model: str = IMAGE_MODEL_NAME,
size_explicit: bool = False,
) -> tuple[list[str], str | None]: ) -> tuple[list[str], str | None]:
"""Orchestriert die Bildgenerierung inkl. Qwen-Hotswap. """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 warning: str | None = None
img.last_error = None img.last_error = None
img.current_model = model img.current_model = model
original_prompt = prompt
prompt_enhancer: dict | None = None
# Qwen wird gestoppt → für Chats nicht verfügbar (die warten). # Qwen wird gestoppt → für Chats nicht verfügbar (die warten).
_set_qwen_unavailable(True) _set_qwen_unavailable(True)
try: try:
@@ -1433,11 +1592,26 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
f"(Exit {proc.returncode}): {out[-500:]}") f"(Exit {proc.returncode}): {out[-500:]}")
_wait_upstream_down(time.monotonic() + 60) _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" img.phase = "loading-image"
worker = _worker() worker = _worker()
# 3) Generieren. # 4) Generieren.
for i in range(n): for i in range(n):
img.phase = "generating" img.phase = "generating"
filename = time.strftime("%Y%m%d-%H%M%S") + \ 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). # Metadaten speichern (Sidecar-JSON).
meta = { meta = {
"prompt": prompt, "prompt": prompt,
"original_prompt": original_prompt,
"prompt_enhancer": prompt_enhancer,
"seed": seed, "seed": seed,
"width": width, "width": width,
"height": height, "height": height,
@@ -1493,7 +1669,7 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
if removed: if removed:
log.info("Bild-Retention: %d alte Bilder entfernt", len(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" img.phase = "unloading-image"
worker.stop() worker.stop()
img.worker = None img.worker = None
@@ -1510,7 +1686,7 @@ def generate_image(prompt: str, width: int, height: int, steps: int,
img.worker = None img.worker = None
raise raise
finally: finally:
# 5) Qwen immer wiederherstellen. # 6) Qwen immer wiederherstellen.
img.phase = "restoring-qwen" img.phase = "restoring-qwen"
try: try:
_restore_qwen(profile) _restore_qwen(profile)
@@ -2358,6 +2534,7 @@ class Handler(BaseHTTPRequestHandler):
return return
# Größe # Größe
size_explicit = "size" in data
size = data.get("size", "1024x1024") size = data.get("size", "1024x1024")
if size not in IMAGE_SIZES: if size not in IMAGE_SIZES:
self._send_error( self._send_error(
@@ -2425,7 +2602,7 @@ class Handler(BaseHTTPRequestHandler):
try: try:
results, warning = generate_image( results, warning = generate_image(
prompt.strip(), width, height, steps, guidance, seed, n, prompt.strip(), width, height, steps, guidance, seed, n,
quality, source_files, model) quality, source_files, model, size_explicit)
except (ValueError, RuntimeError) as e: except (ValueError, RuntimeError) as e:
self._send_error(503, str(e), "server_error", "image_generation_failed") self._send_error(503, str(e), "server_error", "image_generation_failed")
return return
+42 -3
View File
@@ -5,6 +5,8 @@ set -Eeuo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
ENV_FILE=${STACK_ENV:-/etc/mike-ai/stack.env} ENV_FILE=${STACK_ENV:-/etc/mike-ai/stack.env}
MODEL_DIR=${QWEN_IMAGE_21_MODEL_DIR:-/data/models/Qwen-Image-2.1-ComfyUI} MODEL_DIR=${QWEN_IMAGE_21_MODEL_DIR:-/data/models/Qwen-Image-2.1-ComfyUI}
PE_I2I_DIR=${QWEN_IMAGE_PE_I2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-i2i-q5}
PE_T2I_DIR=${QWEN_IMAGE_PE_T2I_MODEL_DIR:-/data/models/qwen-image-2.1-pe-t2i-q5}
BASE=https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/ace0edeb3791a594ddfa36ed5f41a178a394e921 BASE=https://huggingface.co/Comfy-Org/Qwen-Image-2.1/resolve/ace0edeb3791a594ddfa36ed5f41a178a394e921
download() { download() {
@@ -19,15 +21,52 @@ download() {
mv "$target.part" "$target" mv "$target.part" "$target"
} }
download_url() {
local url=$1 target=$2 expected=$3
install -d -m 0755 "$(dirname "$target")"
if [[ -f $target ]] && echo "$expected $target" | sha256sum -c --status; then
echo "OK: $target"
return
fi
curl -fL --retry 5 --retry-all-errors -C - -o "$target.part" "$url"
echo "$expected $target.part" | sha256sum -c --status
mv "$target.part" "$target"
}
download diffusion_models/qwen_image_2.1_int8_convrot.safetensors cb74113cb03faecd79611b01fd7fd642f0aa60d6f0b95086abee214d75eaa57d download diffusion_models/qwen_image_2.1_int8_convrot.safetensors cb74113cb03faecd79611b01fd7fd642f0aa60d6f0b95086abee214d75eaa57d
download text_encoders/qwen3vl_8b_int8_convrot.safetensors 8bfd0f6e12abf2d2d697ecc888e5e90b0d6741d6708f05799f53afa560452e8f download text_encoders/qwen3vl_8b_int8_convrot.safetensors 8bfd0f6e12abf2d2d697ecc888e5e90b0d6741d6708f05799f53afa560452e8f
download vae/qwen_image_2.1_vae_bf16.safetensors bb21f7473051e1ac368515dd3f2e15cd44d7a11748ee8823e1ddca3e4876b7c9 download vae/qwen_image_2.1_vae_bf16.safetensors bb21f7473051e1ac368515dd3f2e15cd44d7a11748ee8823e1ddca3e4876b7c9
# Official Qwen Image 2.1 Prompt Enhancers, quantized to Q5_K_M for the
# 12-GiB RTX 3060. I2I uses the separate multimodal projector; T2I does not.
download_url \
https://huggingface.co/prithivMLmods/Qwen-Image-2.1-PE-I2I-GGUF/resolve/55b9c1a326599e142d59bcad8715d5601ccf8daa/Qwen-Image-2.1-PE-I2I.Q5_K_M.gguf \
"$PE_I2I_DIR/Qwen-Image-2.1-PE-I2I.Q5_K_M.gguf" \
cb71f71fe5fe5938570d65a7c403fbcb1a9065dff9dd9a021f58aec42785b84e
download_url \
https://huggingface.co/prithivMLmods/Qwen-Image-2.1-PE-I2I-GGUF/resolve/55b9c1a326599e142d59bcad8715d5601ccf8daa/Qwen-Image-2.1-PE-I2I.mmproj-bf16.gguf \
"$PE_I2I_DIR/Qwen-Image-2.1-PE-I2I.mmproj-bf16.gguf" \
8dedb71dbc3092dc47de9108ad373d68a12854e2527d59bd9399601738f3bce1
download_url \
https://huggingface.co/Qwen/Qwen-Image-2.1-PE-I2I/resolve/72927bc08afc99b7888ceb7d7d51a12db3700bbd/system_prompt.txt \
"$PE_I2I_DIR/system_prompt.txt" \
e378fea686a1431581ba4c654d332ae96adad633f144ae738ec8ce9c4fd66439
download_url \
https://huggingface.co/prithivMLmods/Qwen-Image-2.1-PE-T2I-GGUF/resolve/e18d4a3e0830ab157770738b16830e6fcf5f57d4/Qwen-Image-2.1-PE-T2I.Q5_K_M.gguf \
"$PE_T2I_DIR/Qwen-Image-2.1-PE-T2I.Q5_K_M.gguf" \
749f5652fd6e8b760ca860091f7a5bffa254a7954203ae5c9bcdf5f93197702f
download_url \
https://huggingface.co/Qwen/Qwen-Image-2.1-PE-T2I/resolve/f3ed7985c788ad75b3ab7223e0c4c51e2a43545b/system_prompt.txt \
"$PE_T2I_DIR/system_prompt.txt" \
a77c9a06c59b120741141d9514b95682bb8761d02bec49ca61def7b2b3d9fb99
compose=(docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/compose.yaml" --profile image --profile flux-standby) compose=(docker compose --env-file "$ENV_FILE" -f "$ROOT_DIR/compose.yaml" --profile image --profile flux-standby)
"${compose[@]}" build image-worker "${compose[@]}" build image-worker
"${compose[@]}" up -d --build --no-deps profile-controller "${compose[@]}" up -d --build --no-deps profile-controller
"${compose[@]}" create image-worker flux-image-worker "${compose[@]}" create image-worker image-prompt-enhancer-i2i \
"${compose[@]}" stop image-worker flux-image-worker image-prompt-enhancer-t2i flux-image-worker
"${compose[@]}" stop image-worker image-prompt-enhancer-i2i \
image-prompt-enhancer-t2i flux-image-worker
state=$(docker inspect -f '{{.State.Status}}' mike-ai-image-worker) state=$(docker inspect -f '{{.State.Status}}' mike-ai-image-worker)
[[ $state == exited || $state == created ]] [[ $state == exited || $state == created ]]
echo "Qwen-Image-2.1 production worker and FLUX standby are prepared and stopped." echo "Qwen-Image-2.1, both prompt enhancers and the FLUX standby are prepared and stopped."