Skill: juan-oy/local-image-gen-aipc (version 1.0.3)

--- name: local-image-gen-aipc #displayName: Local Image Generation (Windows · Z-Image-Turbo · OpenVINO) description: > generate an image, create a picture, draw something, make an image of, text to image, paint a picture, illustrate, visualize, local image generation, AI art, image synthesis, offline image generation, no API key, local inference, generate art, create artwork, produce an image, render an image, AI drawing, image from text. Runs Z-Image-Turbo on-device on Windows via Intel OpenVINO. Prioritizes Intel iGPU (Xe / Arc), falls back to CPU. Bilingual prompts (English + Chinese) supported. SETUP requires network: downloads pip dependencies from GitHub and the model (\~10 GB) from modelscope.cn. INFERENCE is fully offline after setup — no cloud API calls. os: windows requires: * python>=3.10 * git network: setup: required # github.com (pip deps), modelscope.cn (\~10 GB model) inference: offline user-invocable: true allowed-tools: Bash(python *), Bash(powershell *), Bash(git *), Read, Glob, Write, message --- **Model**: `snake7gun/Z-Image-Turbo-int4-ov` (ModelScope INT4) **SKILL_VERSION**: `v1.0.3` > **Network usage**: Setup downloads pip dependencies (some pinned to git+https commits) > from `github.com`, and the model (~10 GB, resume supported) from `modelscope.cn`. > Inference is fully offline — no network calls once setup is complete. **First time?** Before using this skill, run these two scripts once in a terminal: ``` python setup.py # creates venv, installs dependencies (~5 min) python download_model.py # downloads the model (~10 GB, resumable) ``` Both scripts are in the skill directory alongside this SKILL.md. ## Directory layout (all auto-created) ``` {USERNAME}_openvino\ ├── venv\ ← shared venv (created by setup.py) └── imagegen\ ├── state.json ← written by setup.py ├── generate_image.py ← deployed by setup.py (versioned) ├── Z-Image-Turbo-int4-ov\ ← downloaded by download_model.py (~10 GB) └── outputs\YYYYMMDD_HHMMSS_topic.png ``` --- ## Agent instructions 1. **Windows / PowerShell only.** Never use Linux commands (`ls`, `rm`, `cat`). Never use `&&` or `call`. 2. **Every step reads `state.json` itself** — do not pass paths between steps manually. 3. **Use `VENV_PY` from state.json for all inference and package-import calls** — never use system Python for inference or Step 2 DEP_CHECK. Exception: `setup.py`, `check_env.py`, `download_model.py`, and the Step 2 script-write command intentionally run with system Python (they only use stdlib or create/validate the venv, so they must not depend on it). 4. **CRITICAL — Never modify `generate_image.py` manually.** It is deployed and versioned by `setup.py`. If it fails or is missing, re-run `setup.py`. 5. **Goal**: generate an image and send the preview to the conversation. **Auto-recovery policy — try before asking user:** * If `STATE=MISSING`, `VENV_PY=BROKEN`, `PACKAGES_MISSING`, or `SCRIPTS_STALE`: automatically run `setup.py` (up to 3 attempts). Only ask user if all 3 fail. * If `MODEL_STATUS=MISSING`: automatically run `download_model.py` (up to 3 attempts). Stop if a single attempt exceeds 20 minutes — download supports resume, partial progress is not lost. * Always announce before each attempt: `[INFO] Auto-installing environment (attempt N/3)…` **[WARNING] Network / proxy handling** — when running `setup.py` or `download_model.py`: 1. Check for proxy: read `$env:HTTPS_PROXY` / `$env:HTTP_PROXY`; if unset, run `netsh winhttp show proxy`. 2. If proxy detected: apply automatically and continue. 3. If no proxy and download/setup fails: inform the user, suggest configuring one, then retry. Many networks (corporate / China mainland / campus) require a proxy. Download supports resume — safe to retry after fixing network. **Pipeline — follow exactly in order, no skipping:** ``` Step 0: expand prompt → EXPANDED_PROMPT, TOPIC Step 1: verify environment → VENV_PY, IMAGE_GEN_DIR confirmed ready ↳ if STATE=MISSING or VENV_BROKEN or PACKAGES_MISSING: auto-run setup.py (3 attempts) ↳ if MODEL_STATUS=MISSING: auto-run download_model.py (3 attempts) Step 2: verify deps → DEP_CHECK=PASS Step 3: generate + send → [SUCCESS] + image preview ``` ## Pre-flight: Verify Runtime Requirements (Required on First Use) > Pre-flight: Checking Python and git... ### Check Python Version ```powershell python --version ``` **Interpretation:** | Output | Action | |------|------| | `Python 3.10.x` or higher | [OK] `PYTHON_OK`, continue to git check | | `Python 3.8 / 3.9` | Version too low; upgrade is required (see below) | | `'python' is not recognized as an internal or external command` | Python is not installed; install is required (see below) | | `Python was not found; run without arguments...` | **Windows Store alias** — see below | **If output contains "run without arguments to install from the Microsoft Store"**, the Windows Store App Execution Alias is shadowing the real Python. Do NOT ask the user to change settings, and do NOT write helper scripts. Run this command to find the real Python: ```powershell where.exe python 2>$null | Where-Object { $_ -notlike "*WindowsApps*" } | Select-Object -First 1 ``` * A path is printed → **record this literal string as `SYSTEM_PYTHON`** (system-level Python, used only for `setup.py`, `check_env.py`, `download_model.py`). For every command marked `[SYSTEM PYTHON]`, substitute the full literal path for `python`. Do NOT use this path for inference — inference must always use `VENV_PY`. Do NOT use a `$variable` across tool calls — each call is a new shell; always embed the literal path directly. * Nothing printed → Python is not installed — install it (see below). **If Python is missing or outdated**, run this one-command silent installer in PowerShell (recommended, no admin required): ```powershell $f = "$env:TEMP\python-installer.exe" Invoke-WebRequest "https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe" -OutFile $f Start-Process $f -ArgumentList "/quiet InstallAllUsers=0 PrependPath=1 Include_pip=1" -Wait Remove-Item $f ``` > `PrependPath=1` adds Python to PATH automatically; `Include_pip=1` installs pip; `InstallAllUsers=0` avoids requiring administrator privileges. After installation, **restart the terminal**, then run `python --version` and confirm it reports `Python 3.12.x`. If you prefer manual installation: download **https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe** and make sure to check **"Add python.exe to PATH"** during setup. ### Check git ```bat git --version ``` **Interpretation:** | Output | Action | |------|------| | `git version 2.x.x` | [OK] `GIT_OK`, Pre-flight passed | | `'git' is not recognized as an internal or external command` | git is not installed; install is required (see below) | **If git is missing**, run this one-command silent installer in PowerShell: ```powershell $f = "$env:TEMP\git-installer.exe" Invoke-WebRequest "https://github.com/git-for-windows/git/releases/download/v2.49.0.windows.1/Git-2.49.0-64-bit.exe" -OutFile $f Start-Process $f -ArgumentList "/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS=icons,ext\\reg\\shellhere,assoc,assoc_sh" -Wait Remove-Item $f ``` After installation, **restart the terminal**, then run `git --version` to confirm. If you prefer manual installation: open **https://git-scm.com/download/win**, download the installer, and proceed with default options. > git is required for the `git+https://` dependency in `requirements_imagegen.txt`; without git, `pip install` will fail with `git: command not found`. **Pre-flight pass criteria**: `python --version` is >= 3.10 and `git --version` returns a valid version string. Status message: `[OK] Python and git are ready. Starting main workflow.` --- ## Skill Contract (Input / Output) ### Accepted Inputs 1. **Text description** (required) — English or Chinese prompt describing the image to generate. 2. **Optional parameters:** * `steps`: inference steps, default 9 (higher = more detail) * `size`: `512×512` | `768×768` | `1024×1024`, default `512×512` * `seed`: integer (default 42) or `-1` for random * `output`: custom absolute output path for the PNG file ### Output on Success Returns the PNG file path in stdout as `[SUCCESS] <path>`, and sends a preview via the `message` tool. ```json { "image_path": "C:\\...\\outputs\\20260412_153000_panda_bamboo.png", "topic": "panda_bamboo", "prompt": "A giant panda sitting in a lush bamboo forest...", "steps": 9, "size": "512x512", "seed": 42, "device": "GPU.0" } ``` ### Output on Failure ```json { "error": "human-readable description", "stage": "environment | model | inference", "recoverable": true } ``` --- ## Step 0: expand prompt (LLM only — no tools) Do two things simultaneously: **① expand the prompt** and **② extract a topic slug** (English snake_case, used for the filename). Expansion structure: `[subject] [action/pose] [environment] [lighting/mood] [style] [quality tags]` Prompts can be English or Chinese — no translation needed. Topic slug must always be English to avoid path encoding issues. Quality tags: `photorealistic`, `8K resolution`, `cinematic lighting`, `masterpiece` |Input|Topic slug|Expanded prompt| |-|-|-| |a panda|`panda_bamboo`|A giant panda sitting in a lush bamboo forest, sunlight filtering through leaves, photorealistic, 8K, wildlife photography| |赛博朋克城市|`cyberpunk_city`|未来感都市夜景，霓虹灯倒映在湿漉漉的街道，赛博朋克风，电影级，8K| Show the result before proceeding: ``` Input: {user description} Expanded: {full prompt} Topic: {topic_slug} ``` --- ## Step 1: verify environment and model > Step 1/3: checking environment and model… ```powershell # [SYSTEM PYTHON] check_env.py validates the venv — must NOT use venv python here python "<skill_dir>\check_env.py" ``` **Output interpretation:** | Output | Meaning | Action | |---|---|---| | `MODEL_STATUS=READY` + `VENV_PY=...` | OK | Proceed to Step 2 | | `SCRIPTS_STALE=old->new` | `generate_image.py` outdated | Auto-run setup.py (see below) | | `STATE=MISSING` | setup.py never run | Auto-run setup.py (see below) | | `VENV_PY=BROKEN` | venv corrupted | Auto-run setup.py (see below) | | `PACKAGES_MISSING: ...` | packages missing from venv | Auto-run setup.py (see below) | | `MODEL_STATUS=MISSING missing=[...]` | download incomplete | Auto-run download_model.py (see below) | **On success**: record `VENV_PY` and `IMAGE_GEN_DIR` from stdout, proceed to Step 2. > If `SCRIPTS_STALE` also appears alongside `MODEL_STATUS=READY`, auto-run setup.py before Step 2 (see below). --- ### If SCRIPTS_STALE or STATE=MISSING or VENV_PY=BROKEN or PACKAGES_MISSING → auto-run setup.py `SCRIPTS_STALE` means the venv and model are both OK but `generate_image.py` in `IMAGE_GEN_DIR` is an older version. `setup.py` is idempotent — it skips venv/packages if already healthy and only redeploys the script. `PACKAGES_MISSING` means the venv exists but required packages (`openvino`, `torch`, `optimum.intel`) are not installed. Re-running `setup.py` re-installs only what is missing. Announce and run (up to 3 attempts): ``` [INFO] Environment not initialized — auto-installing (attempt 1/3)… ``` ```powershell # [SYSTEM PYTHON] setup.py creates the venv — must NOT use venv python here python "<skill_dir>\setup.py" ``` Re-run `python "<skill_dir>\check_env.py"` after each attempt. If all 3 fail, show manual fallback below. --- ### If MODEL_STATUS=MISSING → auto-run download_model.py Announce to user and ask how to proceed: ``` Model not found — download required (~10 GB) Estimated time: • 100 Mbps → ~15 min • 50 Mbps → ~30 min • 10 Mbps → ~2 hr Download supports resume — safe to interrupt and retry. [Y] Start auto-download [N] I'll download manually — show me the link ``` **Auto-download** (up to 3 attempts, stop if a single attempt exceeds 20 minutes): ```powershell # [SYSTEM PYTHON] download_model.py uses stdlib only — must NOT use venv python here python "<skill_dir>\download_model.py" ``` Re-run `python "<skill_dir>\check_env.py"` after each attempt. **Manual download fallback:** ModelScope page: **https://modelscope.cn/models/snake7gun/Z-Image-Turbo-int4-ov/files** Place all files under `<IMAGE_GEN_DIR>\Z-Image-Turbo-int4-ov\`. Required subdirs: ``` Z-Image-Turbo-int4-ov\ ├── transformer\ ├── vae_decoder\ └── text_encoder\ ``` Then re-run `python "<skill_dir>\check_env.py"` to verify. --- ### Manual fallback (only if all 3 setup auto-attempts fail) Show user: ``` [WARN] Auto-install failed. Please run manually in a terminal: 1) Install environment: python "<skill_dir>\setup.py" Takes ~5 min, fully automated. 2) Download model (~10 GB): python "<skill_dir>\download_model.py" Resumable — safe to interrupt and retry. Come back here when done. ``` --- ## Step 2: verify dependencies > Step 2/3: verifying dependencies… **Verify dependencies** (run via VENV_PY): ``` $env:PYTHONUTF8 = "1" & "<VENV_PY>" -c " import json, site from pathlib import Path EXPECTED_COMMITS = { 'optimum_intel': '2f62e5ae', 'diffusers': 'a1f36ee3', } def get_git_commit(pkg_name): dirs = site.getsitepackages() try: dirs += [site.getusersitepackages()] except Exception: pass for d in dirs: for dist in Path(d).glob(f'{pkg_name}*.dist-info'): url_file = dist / 'direct_url.json' if url_file.exists(): data = json.loads(url_file.read_text(encoding='utf-8')) return data.get('vcs_info', {}).get('commit_id', 'no_vcs_info') return 'not_found' results = {} for pkg, imp in [('openvino','openvino'),('torch','torch'),('Pillow','PIL'),('modelscope','modelscope')]: try: ver = getattr(__import__(imp), '__version__', 'OK') results[pkg] = ('OK', ver) except ImportError as e: results[pkg] = ('MISSING', str(e)) try: from optimum.intel import OVZImagePipeline results['OVZImagePipeline'] = ('OK', 'importable') except ImportError as e: results['OVZImagePipeline'] = ('MISSING', str(e)) for pkg_name, exp in EXPECTED_COMMITS.items(): actual = get_git_commit(pkg_name) if actual == 'not_found': results[f'{pkg_name}@commit'] = ('MISSING', 'not installed via git+https') elif actual.startswith(exp): results[f'{pkg_name}@commit'] = ('OK', actual[:16]) else: results[f'{pkg_name}@commit'] = ('WRONG', f'got {actual[:16]} want {exp}...') all_ok = all(v[0] == 'OK' for v in results.values()) for k, (status, detail) in results.items(): icon = '[OK]' if status == 'OK' else ('[WARN]' if status == 'WRONG' else '[MISSING]') print(f' {icon} {k}: {detail}') print('DEP_CHECK=PASS' if all_ok else 'DEP_CHECK=FAIL') " ``` |Output|Action| |-|-| |`DEP_CHECK=PASS`|[PASS] Proceed to Step 3| |`DEP_CHECK=FAIL` (MISSING)|[FAIL] Re-run `setup.py` and retry| |`DEP_CHECK=FAIL` (`@commit` WRONG)|[FAIL] Force reinstall: `& "<VENV_PY>" -m pip uninstall optimum-intel diffusers -y` then `& "<VENV_PY>" -m pip install -r "<skill_dir>\requirements_imagegen.txt" --no-cache-dir`| --- ## Step 3: generate image and send preview > Step 3/3: running inference… Run these two commands separately: ``` $env:PYTHONUTF8 = "1" ``` ``` & "<VENV_PY>" "<IMAGE_GEN_DIR>\generate_image.py" --prompt "EXPANDED_PROMPT" --topic "TOPIC" --steps 9 --seed 42 ``` **Pass**: stdout contains `[SUCCESS]`. Record `OUTPUT_PATH` from the `[SUCCESS]` line. Send preview via `message` tool: ``` action: "send" filePath: "OUTPUT_PATH" message: "[OK] TOPIC" ``` **Final announcement:** ``` [OK] Done! Path: <OUTPUT_PATH> Prompt: {expanded prompt} steps=9, 512×512, seed=42 | device: {CPU/GPU} ``` --- ## Parameters |Param|Default|Notes| |-|-|-| |`--prompt`|required|English or Chinese| |`--topic`|empty|English snake_case slug for filename| |`--steps`|9|Higher = more detail; no hard limit| |`--width--height`|512|512 | 768|768 1024|1024 recommended| |`--seed`|42|-1 = random| |`--output`|auto|Custom absolute output path| > `guidance_scale` is fixed at `0.0` and not exposed as a parameter. --- ## Troubleshooting |Error|Cause|Fix| |-|-|-| |`STATE=MISSING`|setup.py never run|Run `python "<skill_dir>\setup.py"`| |`VENV_PY=BROKEN`|venv corrupted|Re-run `python "<skill_dir>\setup.py"` — rebuilds venv automatically| |`PACKAGES_MISSING: ...`|venv ok but packages missing|Re-run `python "<skill_dir>\setup.py"` — reinstalls missing packages; skips steps already done| |`MODEL_STATUS=MISSING`|download never run or interrupted|Run `python "<skill_dir>\download_model.py"` — resumes automatically| |`DEP_CHECK=FAIL` (MISSING)|packages not installed in venv|Re-run `setup.py`| |`DEP_CHECK=FAIL` (`@commit` WRONG)|PyPI release installed instead of pinned commit|Uninstall optimum-intel + diffusers, reinstall with `--no-cache-dir`| |`@commit` shows `not installed via git+https`|git was missing when pip ran|Confirm git is installed, re-run `setup.py`| |`[ERROR] Model incomplete`|Download interrupted mid-file|Re-run `download_model.py` — resumes automatically| |`[ERROR] state.json not found`|state.json missing|Re-run Step 1| |`SCRIPTS_STALE=old->new`|`generate_image.py` in `IMAGE_GEN_DIR` is outdated|Auto-run `setup.py` — it redeploys only the script, skips venv/packages/model (fast)| |`RuntimeError` on GPU|Insufficient VRAM|Lower resolution or hardcode `return "CPU"` in `get_device()`| |Black noisy output|Too few steps|Use `--steps` ≥ 4; 9 recommended| |Download timeout|Network issue or proxy needed|Configure proxy and retry — download supports resume| |`RuntimeError: stack expects each tensor to be equal size`|openvino 2026.1.0 breaks `OVZImagePipeline.forward` — `pooled_projections` tensors have mismatched sequence lengths|Downgrade: `& "<VENV_PY>" -m pip install openvino==2026.0.0 --force-reinstall`. Then re-run Step 2 and Step 3. If re-running `setup.py`, it will now install the pinned version automatically.|

path: check_env.py
content: #!/usr/bin/env python
"""Check runtime environment and model availability for the image-gen skill."""

import json
import os
import string
import subprocess
import sys
from pathlib import Path
from threading import Thread


def find_state() -> dict | None:
    """Locate state.json written by setup.py."""
    username = os.environ.get("USERNAME", "user").lower()
    for drive in string.ascii_uppercase:
        state_file = (
            Path(f"{drive}:\\") / f"{username}_openvino" / "imagegen" / "state.json"
        )
        if state_file.exists():
            try:
                return json.loads(state_file.read_text(encoding="utf-8"))
            except Exception:
                pass
    return None


def _read_skill_version() -> str:
    """Read SKILL_VERSION from SKILL.md next to this script — single source of truth."""
    skill_md = Path(__file__).parent / "SKILL.md"
    if skill_md.exists():
        for line in skill_md.read_text(encoding="utf-8").splitlines():
            if "SKILL_VERSION" in line and "`" in line:
                parts = line.split("`")
                if len(parts) >= 2:
                    return parts[1]
    return "unknown"


def _read_deployed_version(script_path: Path) -> str | None:
    """Read SKILL_VERSION embedded in the deployed generate_image.py."""
    if not script_path.exists():
        return None
    for line in script_path.read_text(encoding="utf-8", errors="ignore").splitlines():
        if line.startswith("SKILL_VERSION") and "=" in line:
            return line.split("=")[1].strip().strip("\"'")
    return None


def model_layout_ready(model_dir: Path) -> tuple[bool, list[str], float]:
    """Check model completeness by statting key subdirs and .bin files.

    Returns (ready, missing_dirs, total_gb).
    Each required subdir must exist and contain at least one .bin file >= 1 MB.
    Total GB is computed only when all dirs are present (rglob scan).
    """
    required = ["transformer", "vae_decoder", "text_encoder"]
    missing: list[str] = []
    for name in required:
        subdir = model_dir / name
        if not subdir.is_dir():
            missing.append(name)
            continue
        bins = list(subdir.glob("*.bin"))
        if not bins or max(f.stat().st_size for f in bins) < 1024 * 1024:
            # directory exists but contains no real weights — treat as missing
            missing.append(name)

    if missing:
        return False, missing, 0.0

    total = sum(f.stat().st_size for f in model_dir.rglob("*") if f.is_file()) / 1024**3
    return True, [], total


def check_venv_async(venv_py: Path, result_holder: list) -> None:
    """Validate venv: executable + all key packages importable.

    Runs in a background thread so model stat and venv check can overlap.
    Appends True, False, or a "PACKAGES_MISSING: ..." string to result_holder.
    """
    try:
        # 1. venv Python itself must be executable
        proc = subprocess.run(
            [str(venv_py), "--version"],
            capture_output=True,
            timeout=3,
        )
        if proc.returncode != 0:
            result_holder.append(False)
            return

        # 2. All runtime packages must be importable
        check_script = (
            "import openvino; import torch; import PIL; "
            "import modelscope; from optimum.intel import OVZImagePipeline"
        )
        proc2 = subprocess.run(
            [str(venv_py), "-c", check_script],
            capture_output=True,
            timeout=15,
        )
        if proc2.returncode != 0:
            detail = proc2.stderr.decode(errors="replace").strip()
            result_holder.append(f"PACKAGES_MISSING: {detail}")
            return

        result_holder.append(True)
    except Exception:
        result_holder.append(False)


def main() -> int:
    state = find_state()
    if not state:
        print("STATE=MISSING")
        return 1

    venv_py = Path(state["VENV_PY"])
    imagegen_dir = Path(state["IMAGE_GEN_DIR"])
    model_dir = imagegen_dir / "Z-Image-Turbo-int4-ov"

    # Start venv check in background; stat model files in main thread — both run in parallel
    venv_ok: list = []
    venv_thread = Thread(target=check_venv_async, args=(venv_py, venv_ok), daemon=True)
    venv_thread.start()

    # Model check (main thread — stat only, no imports needed)
    model_ready, missing_dirs, total_gb = model_layout_ready(model_dir)

    # Wait for venv result (model stat already consumed most of the wait time)
    venv_thread.join(timeout=10)

    if not venv_ok or venv_ok[0] is False:
        print("VENV_PY=BROKEN")
        return 1

    if isinstance(venv_ok[0], str):
        print(venv_ok[0])  # PACKAGES_MISSING: <detail>
        return 1

    if not model_ready:
        print(f"MODEL_STATUS=MISSING  missing={missing_dirs}")
        return 1

    print(f"VENV_PY={venv_py}")
    print(f"IMAGE_GEN_DIR={imagegen_dir}")
    print(f"MODEL_STATUS=READY  ({total_gb:.2f} GB)")

    # Script staleness check — compare deployed generate_image.py against SKILL.md
    skill_ver = _read_skill_version()
    deployed_ver = _read_deployed_version(imagegen_dir / "generate_image.py")
    if deployed_ver is None:
        print(f"SCRIPTS_STALE=missing->{skill_ver}")
    elif deployed_ver != skill_ver:
        print(f"SCRIPTS_STALE={deployed_ver}->{skill_ver}")

    return 0


if __name__ == "__main__":
    sys.exit(main())


path: download_model.py
content: """
download_model.py — Download Z-Image-Turbo-int4-ov from ModelScope.

Run once from a terminal (NOT inside OpenClaw):
    python download_model.py

- Downloads ~10 GB to your local {USERNAME}_openvino\\imagegen\\ directory
- Resume supported: safe to Ctrl+C and re-run, will continue from where it stopped
- Run setup.py first if you haven't already
"""

import io, json, os, string, subprocess, sys
from pathlib import Path

# Force UTF-8 stdout/stderr — prevents UnicodeEncodeError on Chinese Windows (CP936)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True)

MODEL_ID = "snake7gun/Z-Image-Turbo-int4-ov"
TOTAL_GB = 10.0

# ── Find state.json ────────────────────────────────────────
def find_state():
    for d in string.ascii_uppercase:
        sf = Path(f"{d}:\\") / f"{os.environ.get('USERNAME', 'user').lower()}_openvino" / "imagegen" / "state.json"
        if sf.exists():
            return json.loads(sf.read_text(encoding="utf-8"))
    return None

state = find_state()
if not state:
    print("[ERROR] state.json not found.")
    print("  Please run setup.py first:")
    print(f"    python \"{Path(__file__).parent / 'setup.py'}\"")
    sys.exit(1)

venv_py = Path(state["VENV_PY"])
if not venv_py.exists():
    print(f"[ERROR] venv not found at {venv_py}")
    print("  Please re-run setup.py.")
    sys.exit(1)

# ── Self-relaunch inside venv ──────────────────────────────
# This ensures modelscope and all deps are available.
if Path(sys.executable).resolve() != venv_py.resolve():
    print(f"[INFO] Switching to venv python: {venv_py}")
    result = subprocess.run([str(venv_py), str(Path(__file__).resolve())])
    sys.exit(result.returncode)

# ─────────────────────────────────────────────────────────────
# From here on we are running inside the venv
# ─────────────────────────────────────────────────────────────
import threading, time
from modelscope import snapshot_download

imagegen_dir = Path(state["IMAGE_GEN_DIR"])
model_dir    = imagegen_dir / "Z-Image-Turbo-int4-ov"

# ── Check if already complete ──────────────────────────────
def get_size_gb(path):
    if not path.exists():
        return 0.0
    return sum(f.stat().st_size for f in path.rglob("*") if f.is_file()) / 1024**3

def model_complete():
    required = ["transformer", "vae_decoder", "text_encoder"]
    return all((model_dir / r).exists() for r in required)

print("=" * 55)
print("  local-image-generation  ·  Model Download")
print("=" * 55)
print(f"\n  Model dir: {model_dir}")

if model_complete():
    gb = get_size_gb(model_dir)
    print(f"\n  Model already complete ({gb:.2f} GB) ✅")
    print("  You can use the skill right away.")
    sys.exit(0)

existing_gb = get_size_gb(model_dir)
if existing_gb > 0.01:
    print(f"\n  Resuming download — {existing_gb:.2f} GB already on disk")
else:
    print(f"\n  Starting fresh download (~{TOTAL_GB} GB)")

print(f"""
  Estimated time:
    100 Mbps → ~15 min
     50 Mbps → ~30 min
     10 Mbps → ~2 hr

  Progress updates every 30 seconds.
  Safe to Ctrl+C and re-run — download will resume.
""")

model_dir.mkdir(parents=True, exist_ok=True)

# ── Download with progress watchdog ───────────────────────
_stop = threading.Event()
t0    = time.time()

def watchdog():
    prev = existing_gb * 1024**3
    print("[Progress] Download started...", flush=True)
    while not _stop.wait(30):
        try:
            total   = sum(f.stat().st_size for f in model_dir.rglob("*") if f.is_file())
            now     = time.time()
            speed   = (total - prev) / 30
            pct     = min(total / (TOTAL_GB * 1024**3) * 100, 99.9)
            elapsed = now - t0
            eta     = (TOTAL_GB * 1024**3 - total) / speed if speed > 0 else 0
            print(
                f"[Progress] {total/1024**3:.2f}/{TOTAL_GB:.1f} GB  "
                f"{pct:.1f}%  {speed/1024**2:.1f} MB/s  "
                f"elapsed {int(elapsed//60)}m{int(elapsed%60):02d}s  "
                f"ETA ~{int(eta//60)}m{int(eta%60):02d}s",
                flush=True
            )
            prev = total
        except Exception:
            pass

threading.Thread(target=watchdog, daemon=True).start()

try:
    snapshot_download(MODEL_ID, local_dir=str(model_dir))
    _stop.set()

    if model_complete():
        gb = get_size_gb(model_dir)
        print(f"\n{'='*55}")
        print(f"  Download complete! ({gb:.2f} GB) ✅")
        print(f"  You can now use the image generation skill in OpenClaw.")
        print(f"{'='*55}")
    else:
        print("\n[WARN] Download finished but model appears incomplete.")
        print("  Re-run this script to resume.")

except KeyboardInterrupt:
    _stop.set()
    print("\n[INFO] Download interrupted.")
    print("  Re-run this script to continue from where it stopped.")

except Exception as e:
    _stop.set()
    err = str(e).lower()
    if any(x in err for x in ["disk", "space"]):
        print(f"\n[ERROR] Disk full: {e}")
        print("  Free up space and re-run.")
    elif any(x in err for x in ["timeout", "connection", "network"]):
        print(f"\n[ERROR] Network error: {e}")
        print("  Check your connection and re-run.")
        print(f"  Manual download: https://modelscope.cn/models/{MODEL_ID}/files")
        print(f"  Place files under: {model_dir}")
        print(f"  Required subdirs: transformer/  vae_decoder/  text_encoder/")
    else:
        print(f"\n[ERROR] {e}")
        print("  Re-run to retry.")
        print(f"  Manual download: https://modelscope.cn/models/{MODEL_ID}/files")
        print(f"  Place files under: {model_dir}")


path: requirements_imagegen.txt
content: --extra-index-url https://download.pytorch.org/whl/cpu

# OpenVINO 推理引擎 — pinned: 2026.1.0 breaks pooled_projections stack in OVZImagePipeline
openvino==2026.0.0

# PyTorch CPU（OVZImagePipeline 内部依赖）
torch==2.8
torchvision==0.23.0

# Optimum-Intel 指定版本（含 OVZImagePipeline，Z-Image-Turbo 必须）
git+https://github.com/openvino-dev-samples/optimum-intel.git@2f62e5aee74b4acba3836e1f26678c0db0a09c00

# Diffusers 指定版本（含 Z-Image-Turbo 调度器支持）
git+https://github.com/huggingface/diffusers.git@a1f36ee3ef4ae1bf98bd260e539197259aa981c1

# 模型下载
modelscope

# 图像保存
Pillow

# 模型加载基础库
transformers
accelerate
huggingface_hub

# 数值计算（Pillow / transformers 约束）
numpy<2.0


path: setup.py
content: """
setup.py — One-time environment setup for local-image-generation skill.

Run once from a terminal:
    python setup.py

What this does:
  1. Creates a shared Python venv under {USERNAME}_openvino\venv\
  2. Checks git is available (required for pinned git+https dependencies)
  3. Installs all required packages into the venv
  4. Writes state.json so the skill knows where everything is
  5. Deploys generate_image.py to IMAGE_GEN_DIR (versioned, idempotent)

After this, run:
    python download_model.py
"""

import io, json, os, shutil, string, subprocess, sys
from pathlib import Path

# Force UTF-8 stdout/stderr — prevents UnicodeEncodeError on Chinese Windows (CP936)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace', line_buffering=True)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace', line_buffering=True)

REQUIREMENTS_FILE = "requirements_imagegen.txt"

PACKAGES_FALLBACK = [
    "openvino==2026.0.0",  # pinned: 2026.1.0 breaks OVZImagePipeline pooled_projections
    "torch>=2.1.0",
    "Pillow>=10.0.0",
    "modelscope>=1.14.0",
    "git+https://github.com/huggingface/optimum-intel.git@2f62e5ae#egg=optimum-intel[openvino]",
    "git+https://github.com/huggingface/diffusers.git@a1f36ee3",
]


def _read_skill_version() -> str:
    """Read SKILL_VERSION from SKILL.md next to this script — single source of truth."""
    skill_md = Path(__file__).parent / "SKILL.md"
    if skill_md.exists():
        for line in skill_md.read_text(encoding="utf-8").splitlines():
            if "SKILL_VERSION" in line and "`" in line:
                parts = line.split("`")
                if len(parts) >= 2:
                    return parts[1]
    return "unknown"

# ── Banner ─────────────────────────────────────────────────
print("=" * 55)
print("  local-image-generation  ·  Environment Setup")
print("=" * 55)

# ── Check Python version ───────────────────────────────────
vi = sys.version_info
if vi < (3, 10):
    print(f"\n[ERROR] Python {vi.major}.{vi.minor} detected — need >= 3.10")
    print("  Download: https://www.python.org/ftp/python/3.12.10/python-3.12.10-amd64.exe")
    sys.exit(1)
print(f"\n  Python {vi.major}.{vi.minor}.{vi.micro} OK ✅")

# ── Check git ──────────────────────────────────────────────
r = subprocess.run(["git", "--version"], capture_output=True)
if r.returncode != 0:
    print("\n[ERROR] git not found — required for pinned git+https dependencies")
    print("  Download: https://git-scm.com/download/win")
    sys.exit(1)
print(f"  {r.stdout.decode().strip()} OK ✅")

# ── Locate root directory ──────────────────────────────────
username  = os.environ.get("USERNAME", "user").lower()
root_name = f"{username}_openvino"
drives    = [f"{d}:\\" for d in string.ascii_uppercase if Path(f"{d}:\\").exists()]

root = next(
    (Path(d) / root_name for d in drives if (Path(d) / root_name).exists()),
    None
)
if not root:
    best = max(drives, key=lambda d: shutil.disk_usage(d).free)
    root = Path(best) / root_name

imagegen_dir = root / "imagegen"
venv_dir     = root / "venv"
venv_py      = venv_dir / "Scripts" / "python.exe"

root.mkdir(parents=True, exist_ok=True)
imagegen_dir.mkdir(parents=True, exist_ok=True)

print(f"\n  Root:     {root}")
print(f"  Imagegen: {imagegen_dir}")
print(f"  Venv:     {venv_dir}")

# ── Create or validate venv ────────────────────────────────
print("\n[1/3] Checking venv...")

venv_ok = False
if venv_py.exists():
    try:
        r = subprocess.run([str(venv_py), "--version"], capture_output=True, timeout=10)
        if r.returncode == 0:
            print(f"  Existing venv OK: {r.stdout.decode().strip()}")
            venv_ok = True
    except Exception:
        pass

if not venv_ok:
    if venv_dir.exists():
        print("  Existing venv is broken — rebuilding...")
        shutil.rmtree(venv_dir, ignore_errors=True)
    print("  Creating venv...")
    subprocess.run([sys.executable, "-m", "venv", str(venv_dir)], check=True)
    venv_py = venv_dir / "Scripts" / "python.exe"
    r = subprocess.run([str(venv_py), "--version"], capture_output=True)
    print(f"  Venv created: {r.stdout.decode().strip()} ✅")

def venv_run(args, **kw):
    return subprocess.run([str(venv_py)] + args, **kw)

# ── Upgrade pip ────────────────────────────────────────────
print("\n[2/3] Upgrading pip...")
venv_run(["-m", "pip", "install", "--upgrade", "pip", "--quiet"], check=True)
print("  pip upgraded ✅")

# ── Install packages ───────────────────────────────────────
print("\n[3/3] Installing packages (this may take ~5 min)...")

req_file = Path(__file__).parent / REQUIREMENTS_FILE
if req_file.exists():
    print(f"  Using {req_file}")
    venv_run(["-m", "pip", "install", "-r", str(req_file)], check=True)
else:
    print(f"  {REQUIREMENTS_FILE} not found — installing fallback list")
    venv_run(["-m", "pip", "install"] + PACKAGES_FALLBACK, check=True)

print("  Packages installed ✅")

# ── Write state.json ───────────────────────────────────────
state = {
    "ROOT":          str(root),
    "IMAGE_GEN_DIR": str(imagegen_dir),
    "VENV_DIR":      str(venv_dir),
    "VENV_PY":       str(venv_py),
    "VENV_EXISTS":   True,
    "SKILL_DIR":     str(Path(__file__).parent.resolve()),
}
state_file = imagegen_dir / "state.json"
state_file.write_text(json.dumps(state, indent=2), encoding="utf-8")
print(f"\n  state.json written: {state_file} ✅")

# ── Create outputs dir ─────────────────────────────────────
(imagegen_dir / "outputs").mkdir(exist_ok=True)

# ── Verify ─────────────────────────────────────────────────
print("\n[Verify] Checking installation...")

verify_script = """
results = {}
for pkg, imp in [
    ("openvino",   "openvino"),
    ("torch",      "torch"),
    ("Pillow",     "PIL"),
    ("modelscope", "modelscope"),
]:
    try:
        ver = getattr(__import__(imp), "__version__", "OK")
        results[pkg] = ("OK", ver)
    except ImportError as e:
        results[pkg] = ("FAIL", str(e))

try:
    from optimum.intel import OVZImagePipeline
    results["OVZImagePipeline"] = ("OK", "importable")
except ImportError as e:
    results["OVZImagePipeline"] = ("FAIL", str(e))

fail = [k for k, (s, _) in results.items() if s == "FAIL"]
for k, (s, d) in results.items():
    icon = "OK  " if s == "OK" else "FAIL"
    print(f"  [{icon}] {k}: {d}")
print()
print("VERIFY=PASS" if not fail else f"VERIFY=FAIL  {fail}")
"""
venv_run(["-c", verify_script])

# ── Deploy generate_image.py ───────────────────────────────
print("\n[Deploy] Deploying generate_image.py...")

SCRIPT_VERSION = _read_skill_version()
script_path = imagegen_dir / "generate_image.py"

existing_ver = None
if script_path.exists():
    for line in script_path.read_text(encoding="utf-8", errors="ignore").splitlines():
        if line.startswith("SKILL_VERSION") and "=" in line:
            existing_ver = line.split("=")[1].strip().strip("\"'")
            break

if existing_ver == SCRIPT_VERSION:
    print(f"  generate_image.py already at {SCRIPT_VERSION} [OK]")
else:
    generate_image_code = r'''SKILL_VERSION = "__VER__"
import sys, io, os, json, string, argparse, re, subprocess
from datetime import datetime
from pathlib import Path

sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace", line_buffering=True)
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace", line_buffering=True)

def get_state():
    for d in string.ascii_uppercase:
        sf = Path(f"{d}:\\") / f"{os.environ.get('USERNAME','user').lower()}_openvino" / "imagegen" / "state.json"
        if sf.exists():
            return json.loads(sf.read_text(encoding="utf-8"))
    return None

def get_device():
    import openvino as ov
    core = ov.Core()
    devs = core.available_devices
    print(f"[INFO] Available devices: {devs}")
    for d in devs:
        if "GPU" in d:
            print(f"[INFO] Using Intel GPU: {d}")
            return d
    print("[INFO] Using CPU")
    return "CPU"

def make_filename(topic, prompt):
    date_str = datetime.now().strftime("%Y%m%d_%H%M%S")
    src = topic if topic else prompt[:30]
    safe = re.sub(r"[^\w]", "_", src.strip())[:30].strip("_")
    return f"{date_str}_{safe}.png"

def generate(prompt, topic="", steps=9, width=512, height=512, seed=42, output_path=None):
    state = get_state()
    if not state:
        print("[ERROR] state.json not found -- run setup.py")
        sys.exit(1)
    imagegen_dir = Path(state["IMAGE_GEN_DIR"])
    model_dir    = imagegen_dir / "Z-Image-Turbo-int4-ov"
    out_dir      = imagegen_dir / "outputs"
    out_dir.mkdir(parents=True, exist_ok=True)
    required = ["transformer", "vae_decoder", "text_encoder"]
    missing  = [r for r in required if not (model_dir / r).exists()]
    if missing:
        print(f"[ERROR] Model incomplete: {missing} -- run download_model.py")
        sys.exit(1)
    device = get_device()
    print(f"[INFO] Loading model: {model_dir}")
    import torch
    from optimum.intel import OVZImagePipeline
    pipe = OVZImagePipeline.from_pretrained(str(model_dir), device=device)
    print("[INFO] Model loaded")
    gen = torch.Generator("cpu").manual_seed(seed) if seed >= 0 else None
    print(f"[INFO] Inference: steps={steps}, {width}x{height}, seed={seed}")
    image = pipe(
        prompt=prompt, height=height, width=width,
        num_inference_steps=steps, guidance_scale=0.0, generator=gen
    ).images[0]
    if output_path is None:
        output_path = str(out_dir / make_filename(topic, prompt))
    image.save(output_path)
    print(f"[SUCCESS] {output_path}")
    try:
        subprocess.Popen(["explorer", output_path])
    except Exception as e:
        print(f"[WARN] Could not open image: {e}")
    return output_path

if __name__ == "__main__":
    try:
        p = argparse.ArgumentParser()
        p.add_argument("--prompt", required=True)
        p.add_argument("--topic",  default="")
        p.add_argument("--steps",  type=int, default=9)
        p.add_argument("--width",  type=int, default=512)
        p.add_argument("--height", type=int, default=512)
        p.add_argument("--seed",   type=int, default=42)
        p.add_argument("--output", default=None)
        args = p.parse_args()
        generate(args.prompt, args.topic, args.steps, args.width, args.height, args.seed, args.output)
        sys.stdout.flush()
    except Exception as e:
        import traceback
        print(f"[FATAL] {type(e).__name__}: {e}", flush=True)
        traceback.print_exc()
        sys.exit(1)
'''
    code = generate_image_code.replace('SKILL_VERSION = "__VER__"', f'SKILL_VERSION = "{SCRIPT_VERSION}"', 1)
    script_path.write_text(code.strip(), encoding="utf-8")
    print(f"  generate_image.py deployed at {SCRIPT_VERSION} [OK]")

# ── Done ───────────────────────────────────────────────────
print()
print("=" * 55)
print("  Setup complete!")
print()
print("  Next step — download the model (~10 GB):")
print(f"    python \"{Path(__file__).parent / 'download_model.py'}\"")
print("=" * 55)


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector:  ()