Integrate Qwen image prompt enhancers
This commit is contained in:
+146
@@ -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.
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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),
|
||||
|
||||
+182
-5
@@ -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
|
||||
|
||||
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user