From 293e23ebb8646a2a4a061fc37e89dc2e2873ddf6 Mon Sep 17 00:00:00 2001 From: taotaojt9988 <103181476+taotaojt9988@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:46:06 +0800 Subject: [PATCH 1/3] Add xiaozhi-media skill (Seedance video / Seedream image / TTS via xzai.me API) --- skills/xiaozhi-media/SKILL.md | 123 ++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 skills/xiaozhi-media/SKILL.md diff --git a/skills/xiaozhi-media/SKILL.md b/skills/xiaozhi-media/SKILL.md new file mode 100644 index 0000000000..25ad546168 --- /dev/null +++ b/skills/xiaozhi-media/SKILL.md @@ -0,0 +1,123 @@ +--- +name: xiaozhi-media +description: Generate Seedance videos, Seedream images, TTS speech, and OpenAI-compatible chat via the Xiaozhi AI (xzai.me) API. Use when the user asks to generate AI video, AI images, text-to-speech, or short video for e-commerce, short drama, or marketing content. +--- +# Xiaozhi AI Media Skill + +Generate videos (Seedance), images (Seedream), speech (TTS), and chat +(OpenAI-compatible) through the Xiaozhi AI (xzai.me) API, from any agent +that supports skills: Claude Code, Codex, Cursor, OpenCode, and similar. + +The skill ships a single dependency-free Python CLI (`scripts/xiaozhi.py`) +plus this instruction file. Everything is plain `urllib` — no SDK, no pip +installs. You only pay for what you generate; the skill itself is free. + +## When to Use + +- "Generate a video from this prompt / make this image move" +- "Generate an image of …" / product shots, posters, illustrations +- "Convert this text to speech" +- You need a multimodal chat model via an OpenAI-compatible endpoint + +Don't use for: music generation, lip-sync, digital humans (not exposed via +the API yet), or video editing of existing footage. + +## Prerequisites + +Two environment variables: + +```bash +export XIAOZHI_API_KEY="your-key" # from xzai.me 个人中心 → API接口 +export XIAOZHI_BASE_URL="https://xzai.me" # optional, this is the default +``` + +**No key yet?** Run any command anyway — the CLI prints the URL +`https://xzai.me` and a 4-step guide (register → 个人中心 → API接口 → copy +key → send it back to the agent). No browser popup; the agent relays the +URL and saves the key once the user provides it. + +## How to Run + +All operations go through the CLI: + +```bash +python scripts/xiaozhi.py video "a corgi running through snow" --ratio 16:9 --out clip.mp4 +python scripts/xiaozhi.py image "minimalist poster, blue and white" --ratio 1:1 --out poster.jpg +python scripts/xiaozhi.py tts "你好,这是语音测试" --voice Cherry --out hello.wav +python scripts/xiaozhi.py chat "reply with ok" +python scripts/xiaozhi.py models +``` + +## Quick Reference + +| Command | What it does | Typical wait | +|---|---|---| +| `video` | Seedance text-to-video / image-to-video | 4–20 min | +| `image` | Seedream 5.0 text-to-image / image reference | ~1 min | +| `tts` | Text to speech (WAV) | seconds | +| `chat` | OpenAI-compatible chat (glm-5.3-flash) | seconds | +| `models` | List available models | — | +| `status ` | Check a video/image task | — | + +## Video Models (the #1 mistake is here) + +| `--model` | duration | notes | +|---|---|---| +| *(omit)* | 10 s | Seedance 2.0 Fast, the default, cheapest | +| `seedance-2.0-fast-15` | 15 s | must be spelled exactly | +| `seedance-2.0-mini` | 15 s | must pass `--duration 15` too | +| `seedance-2.5` | 30 s | only 30 s; also supports 21:9 | + +**The API silently falls back to the 10-second Fast model if the model name +is wrong or `--duration` is missing for mini.** The CLI validates the +combination and refuses to submit an inconsistent pair — never bypass it by +calling the raw endpoint with unvalidated values. + +Ratios: `16:9 / 9:16 / 1:1 / 3:4 / 4:3` (plus `21:9` for 2.5 and fast-15). +Reference images: pass `--images url1 url2` (max 10). One image = first +frame; multiple = multi-reference. + +## Procedure + +1. **Confirm the key works**: `python scripts/xiaozhi.py models`. If this + fails with 401, the key is wrong — stop and check before submitting paid + jobs. +2. **Choose parameters deliberately**: video or image; ratio; duration + (video). State them in your reply so the user can correct before you + submit — generation consumes credits. +3. **Submit and wait**: the CLI polls automatically and prints progress. + Videos take 4–20 minutes; do not re-submit while a task is pending — + duplicate submissions burn credits. Use `status ` in a new call + instead. +4. **Download immediately**: the CLI saves output to `--out`. Result URLs on + the CDN expire; a local copy is the only durable deliverable. Completion + criterion: the file exists on disk and is non-empty. +5. **Report the local path**, not the remote URL, to the user. + +## Image Notes + +- Output size approximates but does not exactly match the requested ratio + (16:9 came out 2848×1600 in testing). If exact dimensions matter, check + the file after download. +- Up to 10 reference image URLs per job via `--images`. +- Styles: pass `--style web_image_style_2D` (also `_3D`, `_portrait`, …). + +## Pitfalls + +- **Two auth schemes**: `api.php` endpoints use the `x-token` header; + `/v1/*` endpoints use `Authorization: Bearer`. The CLI handles this; if + you ever call the API directly, don't mix them. +- **Failed tasks auto-refund** (state 2), so a failed video doesn't cost + credits — but a *duplicate* submission while waiting does. +- **TTS limit**: 1–500 characters per call; longer text must be split. + Audio URLs expire in ~24 h. +- **Context window** of the chat model is 1,048,576 tokens — don't assume + small-context limits, but do not send more than ~1M tokens. +- Errors are standard OpenAI-shaped JSON; 402 means insufficient credits. + +## Verification + +- After any generation, confirm the output file exists and open/inspect it + (e.g. `ffprobe` for video, image dimensions for images). +- `python scripts/xiaozhi.py status ` returns authoritative task + state: 0=pending, 1=done, 2=failed (refunded), -1=not found. From bc7fde36c67e865ac7dad55736a7a8f8b269d529 Mon Sep 17 00:00:00 2001 From: taotaojt9988 <103181476+taotaojt9988@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:46:11 +0800 Subject: [PATCH 2/3] Add xiaozhi-media skill (Seedance video / Seedream image / TTS via xzai.me API) --- skills/xiaozhi-media/scripts/xiaozhi.py | 237 ++++++++++++++++++++++++ 1 file changed, 237 insertions(+) create mode 100644 skills/xiaozhi-media/scripts/xiaozhi.py diff --git a/skills/xiaozhi-media/scripts/xiaozhi.py b/skills/xiaozhi-media/scripts/xiaozhi.py new file mode 100644 index 0000000000..e30cc36858 --- /dev/null +++ b/skills/xiaozhi-media/scripts/xiaozhi.py @@ -0,0 +1,237 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +"""Xiaozhi AI (xzai.me) media CLI — video / image / tts / chat / models / status. + +Dependency-free (stdlib only). Auth: XIAOZHI_API_KEY env var. +api.php endpoints use the `x-token` header; /v1/* endpoints use Bearer. +""" +import argparse +import json +import os +import sys +import time +import urllib.error +import urllib.request + +BASE = os.environ.get("XIAOZHI_BASE_URL", "https://xzai.me").rstrip("/") +KEY = os.environ.get("XIAOZHI_API_KEY", "") + +VIDEO_MODELS = { + None: 10, # default = Seedance 2.0 Fast 10s + "seedance-2.0-fast-15": 15, + "seedance-2.0-mini": 15, + "seedance-2.5": 30, +} +VIDEO_RATIOS = {"16:9", "9:16", "1:1", "3:4", "4:3"} +WIDE_RATIOS = VIDEO_RATIOS | {"21:9"} # 2.5 / fast-15 also support 21:9 +IMAGE_RATIOS = {"1:1", "16:9", "9:16", "3:4", "4:3", "2:3", "3:2"} +TTS_VOICES = {"Cherry", "Ethan", "Chelsie", "Nofish", "Jennifer", "Ryan", "Katerina"} + + +def die(msg, code=2): + print("ERROR:", msg, file=sys.stderr) + sys.exit(code) + + +def need_key(): + if not KEY: + die( + "XIAOZHI_API_KEY is not set. Setup (about 2 minutes):\n" + " 1. Open https://xzai.me and register / log in\n" + " 2. 个人中心 (avatar) -> API 接口\n" + " 3. Copy your API key\n" + " 4. Send it back to the agent, e.g. \"my key is xxx\"\n" + " The agent will save it for you, e.g.:\n" + " export XIAOZHI_API_KEY=\"your-key\" # Linux / macOS\n" + " setx XIAOZHI_API_KEY \"your-key\" # Windows (new terminals)\n" + "Then re-run this command." + ) + return KEY + + +def http_json(path, payload=None, bearer=False, timeout=60): + url = path if path.startswith("http") else BASE + path + if bearer: + headers = {"Content-Type": "application/json", + "Authorization": "Bearer " + need_key()} + else: + headers = {"Content-Type": "application/json", "x-token": need_key()} + data = json.dumps(payload or {}).encode() + req = urllib.request.Request(url, data=data, headers=headers) + try: + with urllib.request.urlopen(req, timeout=timeout) as r: + return json.loads(r.read().decode()) + except urllib.error.HTTPError as e: + body = e.read().decode(errors="replace") + die("HTTP %s from %s\n%s" % (e.code, url, body[:500])) + + +def download(url, out): + req = urllib.request.Request(url, headers={"User-Agent": "xiaozhi-skill/1.0"}) + with urllib.request.urlopen(req, timeout=300) as r, open(out, "wb") as f: + f.write(r.read()) + return os.path.getsize(out) + + +def poll(path, field, task_id, out, interval=20, max_wait=1800, kind="task"): + """Poll a getResult endpoint until done; download the artifact.""" + deadline = time.time() + max_wait + while True: + r = http_json(path, {field: task_id}) + d = r.get("data") or {} + state = d.get("state") + if state == 1: + url = d.get("video_url") or (d.get("images") or [None])[0] + if not url: + die("Task %s finished but no artifact URL in response: %s" % (task_id, r)) + n = download(url, out) + print("saved %s (%d bytes) -> %s" % (kind, n, out)) + if kind == "image": + print("url:", url) + else: + print("url:", url, "(expires; local copy is the durable one)") + return + if state == 2: + die("Task %s FAILED (credits auto-refunded)." % task_id) + if state == -1: + die("Task %s not found." % task_id) + if time.time() > deadline: + print("PENDING: still generating after %ss. Poll later with:" % max_wait) + print(" python %s status %s" % (sys.argv[0], task_id)) + print(" (then download manually from the returned url)") + sys.exit(3) + print("state=0 generating... (%ds elapsed)" % int(time.time() - (deadline - max_wait))) + time.sleep(interval) + + +def cmd_video(args): + model = args.model + if model not in VIDEO_MODELS: + die("model must be one of: %s (got %r). Wrong names silently fall back to fast-10s upstream." % + (", ".join(filter(None, VIDEO_MODELS)), model)) + need_dur = VIDEO_MODELS[model] + if args.duration and args.duration != need_dur: + die("model %s only supports duration=%d (got %d)" % (model, need_dur, args.duration)) + ratio = args.ratio + if ratio not in (WIDE_RATIOS if (model in ("seedance-2.5", "seedance-2.0-fast-15")) else VIDEO_RATIOS): + die("ratio %s not supported for model %s" % (ratio, model)) + payload = {"prompt": args.prompt, "platform": "seedance", "aspect_ratio": ratio} + if model: + payload["model"] = model + if model == "seedance-2.0-mini": + payload["duration"] = 15 # REQUIRED for mini, silently ignored otherwise + if args.images: + payload["images"] = args.images[:10] + r = http_json("/api.php/video/submit", payload) + vid = (r.get("data") or {}).get("video_id") + if not vid: + die("submit failed: %s" % r) + print("submitted video_id=%s model=%s duration=%ds ratio=%s" % + (vid, model or "fast(10s)", need_dur, ratio)) + poll("/api.php/video/getResult", "video_id", vid, args.out, kind="video", + max_wait=40 * 60 if model in ("seedance-2.0-fast-15", "seedance-2.0-mini") else 15 * 60) + + +def cmd_image(args): + if args.ratio not in IMAGE_RATIOS: + die("ratio must be one of %s" % sorted(IMAGE_RATIOS)) + payload = {"message": args.prompt, "aspect_ratio": args.ratio, "is_share": 0} + if args.style: + payload["style_key"] = args.style + if args.images: + payload["images"] = args.images[:10] + r = http_json("/api.php/draw_seedream/seedreamSubmit", payload) + did = (r.get("data") or {}).get("draw_id") + if not did: + die("submit failed: %s" % r) + print("submitted draw_id=%s ratio=%s" % (did, args.ratio)) + poll("/api.php/draw_seedream/getResult", "draw_id", did, args.out, + kind="image", interval=8, max_wait=5 * 60) + + +def cmd_tts(args): + text = args.text + if not (1 <= len(text) <= 500): + die("TTS accepts 1-500 characters per call (got %d). Split longer text." % len(text)) + voice = args.voice + if voice not in TTS_VOICES: + die("voice must be one of %s" % sorted(TTS_VOICES)) + r = http_json("/api.php/tts/text2audio", {"text": text, "voice": voice}) + d = r.get("data") or {} + url = d.get("audio_url") + if not url: + die("tts failed: %s" % r) + n = download(url, args.out) + print("saved audio (%d bytes) -> %s (price=%s credits, chars=%s)" % + (n, args.out, d.get("price"), d.get("characters"))) + print("NOTE: remote url expires in ~24h; local copy saved.") + + +def cmd_chat(args): + messages = [{"role": "user", "content": args.prompt}] + r = http_json("/v1/chat/completions", {"model": args.model, "messages": messages}, bearer=True, timeout=300) + try: + print(r["choices"][0]["message"]["content"] or + "(empty content — see reasoning_content; usage=%s)" % r.get("usage")) + except (KeyError, IndexError): + die("unexpected response: %s" % json.dumps(r)[:500]) + + +def cmd_models(_): + req = urllib.request.Request(BASE + "/v1/models", headers={"Authorization": "Bearer " + need_key()}) + with urllib.request.urlopen(req, timeout=30) as r: + d = json.loads(r.read().decode()) + for m in d.get("data", []): + print(m.get("id")) + + +def cmd_status(args): + r = http_json("/api.php/video/getResult", {"video_id": int(args.task_id)}) + print(json.dumps(r, ensure_ascii=False, indent=2)) + + +def main(): + p = argparse.ArgumentParser(description="Xiaozhi AI media CLI") + sub = p.add_subparsers(dest="cmd", required=True) + + v = sub.add_parser("video", help="generate a video (Seedance)") + v.add_argument("prompt") + v.add_argument("--model", default=None, help="omit=fast10s | seedance-2.0-fast-15 | seedance-2.0-mini | seedance-2.5") + v.add_argument("--ratio", default="16:9") + v.add_argument("--duration", type=int, default=None) + v.add_argument("--images", nargs="*", help="reference image URLs (max 10)") + v.add_argument("--out", default="output.mp4") + v.set_defaults(fn=cmd_video) + + i = sub.add_parser("image", help="generate an image (Seedream)") + i.add_argument("prompt") + i.add_argument("--ratio", default="1:1") + i.add_argument("--style", default=None) + i.add_argument("--images", nargs="*", help="reference image URLs (max 10)") + i.add_argument("--out", default="output.jpg") + i.set_defaults(fn=cmd_image) + + t = sub.add_parser("tts", help="text to speech") + t.add_argument("text") + t.add_argument("--voice", default="Cherry") + t.add_argument("--out", default="output.wav") + t.set_defaults(fn=cmd_tts) + + c = sub.add_parser("chat", help="OpenAI-compatible chat") + c.add_argument("prompt") + c.add_argument("--model", default="glm-5.3-flash") + c.set_defaults(fn=cmd_chat) + + m = sub.add_parser("models", help="list models") + m.set_defaults(fn=cmd_models) + + s = sub.add_parser("status", help="check video task status") + s.add_argument("task_id") + s.set_defaults(fn=cmd_status) + + args = p.parse_args() + args.fn(args) + + +if __name__ == "__main__": + main() From 4d948928b7c6633bf42f8735ddcfa72c915d9ae6 Mon Sep 17 00:00:00 2001 From: taotaojt9988 <103181476+taotaojt9988@users.noreply.github.com> Date: Wed, 28 Oct 2026 08:46:14 +0800 Subject: [PATCH 3/3] Add xiaozhi-media skill (Seedance video / Seedream image / TTS via xzai.me API) --- skills/xiaozhi-media/references/api.md | 90 ++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 skills/xiaozhi-media/references/api.md diff --git a/skills/xiaozhi-media/references/api.md b/skills/xiaozhi-media/references/api.md new file mode 100644 index 0000000000..c9923c5699 --- /dev/null +++ b/skills/xiaozhi-media/references/api.md @@ -0,0 +1,90 @@ +# Xiaozhi AI API reference (for direct integration) + +The CLI covers common cases. This file documents the raw endpoints for +agents that prefer calling the API directly (SDKs, MCP servers, workflows). + +## Auth (two schemes — never mix) + +| Endpoint family | Header | +|---|---| +| `https://xzai.me/api.php/*` | `x-token: ` | +| `https://xzai.me/v1/*` (OpenAI-compatible) | `Authorization: Bearer ` | + +## Endpoints + +| Capability | Endpoint | +|---|---| +| Model list | `GET /v1/models` (Bearer) | +| Chat (OpenAI-compatible: tools, stream, multimodal) | `POST /v1/chat/completions` (Bearer) | +| Chat (native) | `POST /api.php/chat/sendText` (x-token) | +| Image gen (Seedream 5.0) | `POST /api.php/draw_seedream/seedreamSubmit` → `/api.php/draw_seedream/getResult` | +| Video gen (Seedance) | `POST /api.php/video/submit` → `/api.php/video/getResult` | +| TTS | `POST /api.php/tts/text2audio` | +| Image upload (get reference URL) | `POST /api.php/upload/image` (multipart, field `image`) | + +## Image submit + +```json +{ + "message": "prompt text (max 2000 chars)", + "aspect_ratio": "1:1 | 16:9 | 9:16 | 3:4 | 4:3 | 2:3 | 3:2", + "style_key": "web_image_style_2D | web_image_style_3D | web_image_style_portrait | ...", + "images": ["https://.../ref.jpg"], + "cate_id": 0, + "is_share": 0 +} +``` + +Response: `{"errno":0,"data":{"draw_id":3305}}`. Poll getResult with +`{"draw_id": N}`. + +Output size approximates the ratio but is not exact (16:9 → 2848×1600 in +testing; 1:1 → 2048×2048). + +## Video submit + +```json +{ + "prompt": "...", + "platform": "seedance", + "model": "seedance-2.0-mini", + "aspect_ratio": "16:9", + "duration": 15, + "images": ["https://.../first_frame.jpg"] +} +``` + +- Omitting `model` = Seedance 2.0 Fast 10s (cheapest). +- `seedance-2.0-fast-15` / `seedance-2.0-mini`: 15 s, must be exact; wrong + or missing params silently fall back to fast-10s. +- `seedance-2.5`: 30 s only; also allows 21:9; takes 4–7 min. +- fast-15 / mini take 10–20 min. +- `images`: omitted = t2v; 1 image = first-frame drive; up to 10 = multi-ref. + +## Task states (video & image) + +`state: 0 = generating, 1 = done (video_url / images[]), 2 = failed (auto-refund), -1 = not found` + +## TTS + +`{"text": "1-500 chars", "voice": "Cherry|Ethan|Chelsie|Nofish|Jennifer|Ryan|Katerina"}` +→ `data.audio_url` (WAV, expires ~24 h), `data.price`. 10 credits / 10k +chars, min 1 credit per call, no charge on failure. + +## Billing + +- Failed generation tasks auto-refund (state 2). +- Chat: charged only on success; text35 tier (native) / text40 per request + (OpenAI-compatible path). +- OpenAI-compatible errors are standard OpenAI error JSON: + 400 invalid params, 401 bad key, 402 insufficient_balance, 403 channel + closed, 404 model_not_found, 429 rate limit, 502/504 upstream. + +## Chat model + +Currently `glm-5.3-flash`. Context window: 1,048,576 tokens (verified +empirically; 1024k-token inputs succeed, ~1.15M rejected with 400). +Supports function calling, SSE streaming, multimodal `content` arrays. + +Use with the OpenAI SDK: `base_url="https://xzai.me/v1"`, +`api_key=`.