Skill: eric51/ollama-x-z-image-turbo (version 1.0.0)

# Ollama x/z-image-turbo Skill ## Description Génère des images via **Ollama** (modèle `x/z-image-turbo`) et les envoie sur WhatsApp. ## Déclenchement Quand l'utilisateur demande de **générer/créer/dessiner une image** (ou variantes), suivre ces étapes : ### Étape 1 — Générer l'image Exécuter via `exec` avec **pty=true** (obligatoire) : ```bash python3 /Users/openclaw/.openclaw/skills/ollama-x-z-image-turbo/runner.py \ --prompt "<PROMPT>" \ --width 1024 --height 1024 --steps 20 \ --out /Users/openclaw/.openclaw/workspace/tmp/ollama_image.png -v ``` **Important** : - `pty=true` est **obligatoire** (Ollama nécessite un TTY) - `timeout=120` minimum recommandé - Ajuster `--width`, `--height`, `--steps` selon la demande ### Étape 2 — Envoyer sur WhatsApp Utiliser l'outil **message** : ``` action: send channel: whatsapp to: <numéro> message: <légende> filePath: /Users/openclaw/.openclaw/workspace/tmp/ollama_image.png ``` ## Paramètres disponibles | Paramètre | Défaut | Description | |-------------|--------|---------------------------------| | --prompt | requis | Description de l'image | | --width | 1024 | Largeur en pixels | | --height | 1024 | Hauteur en pixels | | --steps | 20 | Étapes de débruitage | | --seed | aléa | Graine pour reproductibilité | | --negative | aucun | Prompt négatif | | --timeout | 300 | Timeout en secondes | ## Exemples de demandes utilisateur - "Génère une image d'un chat astronaute" - "Crée-moi une illustration d'un coucher de soleil" - "Dessine un robot dans une forêt" - "Fais une image de …" ## Prérequis - Ollama actif sur `http://127.0.0.1:11434` - Modèle installé : `x/z-image-turbo:latest`

path: generate_image.py
content: import base64
import os
import requests
from fastapi import FastAPI, HTTPException
from fastapi.responses import Response
from pydantic import BaseModel

app = FastAPI()

OLLAMA_URL = os.getenv("OLLAMA_URL", "http://127.0.0.1:11434")
MODEL = os.getenv("OLLAMA_IMAGE_MODEL", "x/z-image-turbo:latest")

class ImageRequest(BaseModel):
    prompt: str
    width: int = 1024
    height: int = 1024
    steps: int = 20
    seed: int | None = None
    negative: str | None = None

@app.post("/generate-image/", response_class=Response)
async def generate_image(request: ImageRequest):
    url = f"{OLLAMA_URL}/api/generate"
    payload = {
        "model": MODEL,
        "prompt": request.prompt,
        "stream": False,
        "options": {
            "width": request.width,
            "height": request.height,
            "steps": request.steps,
        },
    }
    if request.seed is not None:
        payload["options"]["seed"] = request.seed
    if request.negative:
        payload["options"]["negative"] = request.negative

    r = requests.post(url, json=payload, timeout=180)
    if r.status_code != 200:
        raise HTTPException(status_code=500, detail=r.text)

    data = r.json()
    # Ollama image models return base64 in images[]
    if "images" in data and data["images"]:
        img_b64 = data["images"][0]
    elif "response" in data:
        img_b64 = data["response"]
    else:
        raise HTTPException(status_code=500, detail="No image data in response")

    try:
        img = base64.b64decode(img_b64)
    except Exception:
        raise HTTPException(status_code=500, detail="Invalid image base64")

    return Response(content=img, media_type="image/png")


path: runner.py
content: #!/usr/bin/env python3
"""Generate images via Ollama x/z-image-turbo with verbose logging."""

import argparse
import base64
import json
import logging
import os
import re
import shutil
import subprocess
import sys
import time
from pathlib import Path

DEFAULT_MODEL = "x/z-image-turbo:latest"
PNG_MAGIC = b"\x89PNG\r\n\x1a\n"
SAVE_RE = re.compile(r"Image saved to:\s*(.+)")

logging.basicConfig(
    level=logging.DEBUG,
    format="%(asctime)s [%(levelname)s] %(message)s",
    datefmt="%H:%M:%S",
)
log = logging.getLogger("ollama-image")


def _write_bytes(out_path: Path, data: bytes):
    out_path.parent.mkdir(parents=True, exist_ok=True)
    out_path.write_bytes(data)
    log.info("Wrote %d bytes to %s", len(data), out_path)


def _try_parse_json(output: str):
    """Parse JSON from ollama --format json output (may be multiple lines)."""
    images = []
    for i, line in enumerate(output.splitlines()):
        line = line.strip()
        if not line:
            continue
        try:
            obj = json.loads(line)
        except json.JSONDecodeError:
            log.debug("Line %d not JSON: %.80s…", i, line)
            continue
        log.debug("Line %d JSON keys: %s", i, list(obj.keys()) if isinstance(obj, dict) else type(obj))
        if isinstance(obj, dict):
            if "images" in obj and isinstance(obj["images"], list):
                images.extend([x for x in obj["images"] if isinstance(x, str)])
            if "image" in obj and isinstance(obj["image"], str):
                images.append(obj["image"])
            if "response" in obj and isinstance(obj["response"], str):
                images.append(obj["response"])
    log.info("Parsed %d image candidate(s) from JSON", len(images))
    return images


def _maybe_saved_path(output: str):
    for line in output.splitlines():
        m = SAVE_RE.search(line.strip())
        if m:
            path = m.group(1).strip()
            log.info("Detected 'Image saved to' path: %s", path)
            return path
    return None


def generate(prompt, out, width=1024, height=1024, steps=20, seed=None, negative=None, model=DEFAULT_MODEL, timeout=180):
    cmd = [
        "ollama", "run", model, prompt,
        "--width", str(width),
        "--height", str(height),
        "--steps", str(steps),
    ]
    if seed is not None:
        cmd += ["--seed", str(seed)]
    if negative:
        cmd += ["--negative", negative]

    # Run from the output directory so relative "Image saved to:" paths land there
    cwd = str(out.parent)
    os.makedirs(cwd, exist_ok=True)

    log.info("Command: %s", " ".join(cmd))
    log.info("CWD: %s", cwd)
    log.info("Timeout: %ds", timeout)

    t0 = time.time()
    try:
        res = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=cwd)
    except subprocess.TimeoutExpired:
        elapsed = time.time() - t0
        log.error("Timeout after %.1fs", elapsed)
        raise RuntimeError(f"Ollama timeout after {elapsed:.0f}s")

    elapsed = time.time() - t0
    log.info("Process exited in %.1fs with code %d", elapsed, res.returncode)
    log.info("stdout length: %d chars", len(res.stdout))
    log.info("stderr length: %d chars", len(res.stderr))

    if res.stdout:
        log.debug("stdout first 500 chars: %s", res.stdout[:500])
    if res.stderr:
        log.debug("stderr first 500 chars: %s", res.stderr[:500])

    if res.returncode != 0:
        raise RuntimeError(f"Ollama error (code {res.returncode}): {res.stderr.strip() or res.stdout.strip()}")

    # 1) Ollama saves directly to file and prints path
    saved_path = _maybe_saved_path(res.stdout) or _maybe_saved_path(res.stderr)
    # Resolve relative paths against cwd
    if saved_path and not os.path.isabs(saved_path):
        saved_path = os.path.join(cwd, saved_path)
    if saved_path and os.path.exists(saved_path):
        log.info("Found saved file at %s (%d bytes)", saved_path, os.path.getsize(saved_path))
        out.parent.mkdir(parents=True, exist_ok=True)
        shutil.copyfile(saved_path, out)
        log.info("Copied to %s", out)
        return out

    # 2) Try JSON base64
    images_b64 = _try_parse_json(res.stdout)
    if images_b64:
        try:
            data = base64.b64decode(images_b64[0])
            log.info("Decoded base64: %d bytes, starts with PNG magic: %s", len(data), data[:8] == PNG_MAGIC)
            if data.startswith(PNG_MAGIC):
                _write_bytes(out, data)
                return out
        except Exception as e:
            log.warning("Base64 decode failed: %s", e)

    # 3) Fallback: raw stdout as PNG
    raw = res.stdout.encode()
    log.debug("Raw stdout first 16 bytes: %s", raw[:16])
    if raw.startswith(PNG_MAGIC):
        log.info("Raw stdout is PNG (%d bytes)", len(raw))
        _write_bytes(out, raw)
        return out

    raise RuntimeError("No image data detected in ollama output")


def main():
    ap = argparse.ArgumentParser(description="Generate image with ollama x/z-image-turbo")
    ap.add_argument("--prompt", required=True)
    ap.add_argument("--out", required=True)
    ap.add_argument("--width", type=int, default=1024)
    ap.add_argument("--height", type=int, default=1024)
    ap.add_argument("--steps", type=int, default=20)
    ap.add_argument("--seed", type=int, default=None)
    ap.add_argument("--negative", default=None)
    ap.add_argument("--model", default=DEFAULT_MODEL)
    ap.add_argument("--timeout", type=int, default=300)
    ap.add_argument("-v", "--verbose", action="store_true", help="Extra verbose output")
    args = ap.parse_args()

    if args.verbose:
        logging.getLogger().setLevel(logging.DEBUG)

    out_path = Path(args.out)
    try:
        result = generate(
            prompt=args.prompt,
            out=out_path,
            width=args.width,
            height=args.height,
            steps=args.steps,
            seed=args.seed,
            negative=args.negative,
            model=args.model,
            timeout=args.timeout,
        )
        log.info("SUCCESS: %s", result)
        print(str(result))
    except Exception as e:
        log.error("FAILED: %s", e)
        print(f"ERROR: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == "__main__":
    main()


path: test_api_call.py
content: import requests

# Définir l'URL d'Ollama
url = 'http://127.0.0.1:11434/api/generate'  # ou l'endpoint approprié

# Exemple de requête pour le modèle
payload = {"model": "x/z-image-turbo", "prompt": "a cartoon cat astronaut on the moon, with big eyes and colorful fur"}

# Appel de l'API
response = requests.post(url, json=payload)

# Affichage du résultat
if response.status_code == 200:
    print("Image générée avec succès:", response.json())
else:
    print("Erreur lors de la génération d'image:", response.text)

path: whatsapp_integration.py
content: import requests
import json
from fastapi import FastAPI

app = FastAPI()

@app.post("/send-to-whatsapp/")
async def send_to_whatsapp(image_url: str, message: str, recipient: str):
    # Logique pour envoyer l'image à WhatsApp
    whatsapp_api_url = "https://api.whatsapp.com/send"
    payload = {
        "phone": recipient,
        "body": f"{message} \n Voici votre image : {image_url}"
    }

    response = requests.post(whatsapp_api_url, data=json.dumps(payload), headers={'Content-Type': 'application/json'})
    return response.json()

path: whatsapp_routes.yaml
content: # Routes pour l'intégration WhatsApp

route: /send-to-whatsapp/


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