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