From 5106d0d2ed176aaf2e495edb6bdfb61407058688 Mon Sep 17 00:00:00 2001 From: Mikei386 <44135113+Mikei386@users.noreply.github.com> Date: Mon, 21 Sep 2026 14:51:29 +0200 Subject: [PATCH] Integrate Qwen image prompt enhancers --- compose.yaml | 146 ++++++++++++++ dev/test_router_coordination.py | 12 ++ .../profile-controller/profile_controller.py | 23 ++- router/ai_profile_router.py | 187 +++++++++++++++++- scripts/prepare-qwen-image-21.sh | 45 ++++- 5 files changed, 403 insertions(+), 10 deletions(-) diff --git a/compose.yaml b/compose.yaml index f5a8d02..168739d 100644 --- a/compose.yaml +++ b/compose.yaml @@ -572,6 +572,8 @@ services: ALLOWED_PROFILES: fast,medium,large,ultra,uncensored IMAGE_WORKER: image RESTORE_WORKER: restore + IMAGE_PROMPT_I2I_WORKER: image-prompt-i2i + IMAGE_PROMPT_T2I_WORKER: image-prompt-t2i FLUX_STANDBY_WORKER: flux-standby TTS_WORKER: qwen3 MUSIC_WORKER: acestep @@ -602,6 +604,8 @@ services: volumes: - ./router/router_profiles.json:/etc/mike-ai/router-profiles.json: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-images:/data/images environment: @@ -631,6 +635,11 @@ services: IMAGE_DIR: /data/images IMAGE_WORKER_URL: http://image-worker:8086 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_INFERENCE_STEPS: "25" CHAT_IMAGE_ALLOW_REMOTE_URLS: "false" @@ -716,6 +725,143 @@ services: retries: 90 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. # It is outside the normal router path and can only be started through the # controller's allowlisted flux-standby endpoint. diff --git a/dev/test_router_coordination.py b/dev/test_router_coordination.py index a8b2f30..163dec2 100644 --- a/dev/test_router_coordination.py +++ b/dev/test_router_coordination.py @@ -13,6 +13,18 @@ import ai_profile_router as router 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): with patch.object(router, 'PROFILE_CONTROL_URL', 'http://controller'), \ patch.object(router, '_profile_controller_request', return_value={ diff --git a/platform/docker/profile-controller/profile_controller.py b/platform/docker/profile-controller/profile_controller.py index 47a243e..43d3020 100644 --- a/platform/docker/profile-controller/profile_controller.py +++ b/platform/docker/profile-controller/profile_controller.py @@ -27,6 +27,10 @@ LABEL_KEY = "com.mike-ai.llama-profile" IMAGE_LABEL_KEY = "com.mike-ai.image-worker" IMAGE_WORKER = os.environ.get("IMAGE_WORKER", "image") 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", "flux-standby").strip() 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]: """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: allowed.add(FLUX_STANDBY_WORKER) 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: - 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") with LOCK: item = image_container(kind) @@ -799,6 +814,10 @@ class Handler(BaseHTTPRequestHandler): worker_paths = { "/workers/image/start": (IMAGE_WORKER, True), "/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/stop": (RESTORE_WORKER, False), "/workers/flux-standby/start": (FLUX_STANDBY_WORKER, True), diff --git a/router/ai_profile_router.py b/router/ai_profile_router.py index 5e3285b..61710aa 100755 --- a/router/ai_profile_router.py +++ b/router/ai_profile_router.py @@ -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 diff --git a/scripts/prepare-qwen-image-21.sh b/scripts/prepare-qwen-image-21.sh index 72b0a48..2db38c7 100755 --- a/scripts/prepare-qwen-image-21.sh +++ b/scripts/prepare-qwen-image-21.sh @@ -5,6 +5,8 @@ set -Eeuo pipefail ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" ENV_FILE=${STACK_ENV:-/etc/mike-ai/stack.env} 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 download() { @@ -19,15 +21,52 @@ download() { 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 text_encoders/qwen3vl_8b_int8_convrot.safetensors 8bfd0f6e12abf2d2d697ecc888e5e90b0d6741d6708f05799f53afa560452e8f 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[@]}" build image-worker "${compose[@]}" up -d --build --no-deps profile-controller -"${compose[@]}" create image-worker flux-image-worker -"${compose[@]}" stop image-worker flux-image-worker +"${compose[@]}" create image-worker image-prompt-enhancer-i2i \ + 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 == 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."