Qwen Audio TTS (Text-to-Speech) Synthesize natural speech from text using Qwen TTS models. This skill is part of qwencloud/qwencloud-ai . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details. | Location | Purpose | |----------|---------| | | Qwen TTS / Qwen Audio TTS (HTTP and WebSocket); current compatibility and defaults are in the model catalog below | | | CosyVoice (WebSocket API) — requires SDK; current compatibility and defaults are in the model catalog below | | | CosyVoice setup, voices, ex…

\n for candidate in candidates:\n m = re.search(pattern, candidate)\n if m:\n agent, skill = m.group(1) or \"default\", m.group(2)\n if len(agent) \u003c= 32 and len(skill) \u003c= 32:\n return json.dumps({\"channel\": \"qwencloud-skill\", \"tags\": {\"t1\": skill, \"t2\": agent}}, separators=(',', ':'))\n return None\n\n\n# ---------------------------------------------------------------------------\n# Section 6: HTTP infrastructure\n# ---------------------------------------------------------------------------\n\n_RETRYABLE_CODES = frozenset({429, 500, 502, 503, 504})\n\n\ndef http_request(\n method: str,\n url: str,\n api_key: str,\n payload: dict[str, Any] | None = None,\n *,\n extra_headers: dict[str, str] | None = None,\n timeout: int = 120,\n retries: int = 2,\n backoff: float = 1.5,\n) -> dict[str, Any]:\n \"\"\"Generic HTTP request with retry and exponential backoff.\n\n Handles JSON serialisation, provider header injection, and retryable\n HTTP codes.\n \"\"\"\n hdrs = get_provider().make_headers(api_key, payload)\n if extra_headers:\n hdrs.update(extra_headers)\n data = json.dumps(payload).encode(\"utf-8\") if payload else None\n last_err = \"\"\n for attempt in range(retries + 1):\n req = urllib.request.Request(url, data=data, headers=hdrs, method=method)\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n return json.loads(resp.read().decode(\"utf-8\"))\n except urllib.error.HTTPError as exc:\n body = \"\"\n try:\n body = exc.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n if api_key and len(api_key) > 8 and api_key in body:\n body = body.replace(api_key, mask_key(api_key))\n last_err = f\"HTTP {exc.code}: {sanitize_diagnostic(body)[:500]}\"\n if exc.code not in _RETRYABLE_CODES or attempt >= retries:\n if exc.code == 401 and is_token_plan_key(api_key):\n print(\n \"Hint: if this model is not on Token Plan, see supported \"\n \"models at https://docs.qwencloud.com/token-plan\",\n file=sys.stderr,\n )\n raise RuntimeError(last_err) from None\n time.sleep(backoff * (2 ** attempt))\n except urllib.error.URLError as exc:\n last_err = sanitize_diagnostic(str(exc.reason))\n if attempt >= retries:\n raise RuntimeError(f\"Network error: {last_err}\") from None\n time.sleep(backoff * (2 ** attempt))\n raise RuntimeError(last_err)\n\n\ndef http_post(\n url: str,\n api_key: str,\n payload: dict[str, Any],\n *,\n timeout: int = 120,\n retries: int = 2,\n backoff: float = 1.5,\n) -> dict[str, Any]:\n \"\"\"Convenience: non-streaming POST, returns parsed JSON.\"\"\"\n return http_request(\n \"POST\", url, api_key, payload,\n timeout=timeout, retries=retries, backoff=backoff,\n )\n\n\ndef _sse_error_message(\n chunk: Any,\n event_type: str,\n api_key: str,\n) -> Optional[str]:\n \"\"\"Return a safe error message for an SSE error payload, if present.\"\"\"\n error_payload = chunk.get(\"error\") if isinstance(chunk, dict) else None\n has_top_level_error = isinstance(chunk, dict) and (\n not chunk.get(\"choices\")\n and chunk.get(\"code\") is not None\n and chunk.get(\"message\") is not None\n )\n if event_type.lower() != \"error\" and error_payload is None and not has_top_level_error:\n return None\n\n payload = error_payload if error_payload is not None else chunk\n if isinstance(payload, dict):\n code = payload.get(\"code\") or payload.get(\"type\")\n message = payload.get(\"message\")\n if code and message:\n detail = f\"{code}: {message}\"\n elif code or message:\n detail = str(code or message)\n else:\n detail = json.dumps(payload, ensure_ascii=False)\n else:\n detail = str(payload)\n\n if api_key and len(api_key) > 8 and api_key in detail:\n detail = detail.replace(api_key, mask_key(api_key))\n return f\"SSE error: {sanitize_diagnostic(detail)[:500]}\"\n\n\ndef stream_sse(\n url: str,\n api_key: str,\n payload: dict[str, Any],\n *,\n timeout: int = 180,\n) -> Iterator[dict[str, Any]]:\n \"\"\"Streaming POST. Yields parsed SSE ``data:`` chunks.\n\n Automatically sets ``stream: true`` in the payload and sends the\n appropriate ``Accept`` header.\n \"\"\"\n payload[\"stream\"] = True\n hdrs = get_provider().make_headers(api_key, payload)\n hdrs[\"Accept\"] = \"text/event-stream\"\n data = json.dumps(payload).encode(\"utf-8\")\n req = urllib.request.Request(url, data=data, headers=hdrs, method=\"POST\")\n try:\n resp = urllib.request.urlopen(req, timeout=timeout)\n except urllib.error.HTTPError as exc:\n body = \"\"\n try:\n body = exc.read().decode(\"utf-8\", errors=\"replace\")\n except Exception:\n pass\n if api_key and len(api_key) > 8 and api_key in body:\n body = body.replace(api_key, mask_key(api_key))\n raise RuntimeError(f\"HTTP {exc.code}: {sanitize_diagnostic(body)[:500]}\") from None\n except urllib.error.URLError as exc:\n raise RuntimeError(f\"Network error: {sanitize_diagnostic(str(exc.reason))}\") from None\n\n buf = b\"\"\n event_type = \"\"\n try:\n while True:\n raw = resp.read(4096)\n if not raw:\n break\n buf += raw\n while b\"\\n\" in buf:\n line_bytes, buf = buf.split(b\"\\n\", 1)\n line = line_bytes.decode(\"utf-8\", errors=\"replace\").strip()\n if not line:\n event_type = \"\"\n continue\n if line.startswith(\"event:\"):\n event_type = line[6:].strip()\n continue\n if not line.startswith(\"data:\"):\n continue\n json_str = line[5:].strip()\n if json_str == \"[DONE]\":\n return\n try:\n chunk = json.loads(json_str)\n except json.JSONDecodeError:\n if event_type.lower() == \"error\":\n detail = json_str\n if api_key and len(api_key) > 8 and api_key in detail:\n detail = detail.replace(api_key, mask_key(api_key))\n detail = sanitize_diagnostic(detail)[:500]\n raise RuntimeError(f\"SSE error: {detail}\") from None\n pass\n else:\n error_message = _sse_error_message(chunk, event_type, api_key)\n if error_message:\n raise RuntimeError(error_message)\n yield chunk\n event_type = \"\"\n finally:\n resp.close()\n\n\n# ---------------------------------------------------------------------------\n# Section 7: File upload / download\n# ---------------------------------------------------------------------------\n\n_BASE64_FILE_LIMIT = 7 * 1024 * 1024 # 7 MB (base64 adds ~33%; API limit is 10 MB)\n\n\ndef upload_local_file(api_key: str, model: str, fp: Path) -> str:\n \"\"\"Upload a local file to the active provider's temp storage.\n\n Returns a provider-managed URL (e.g. ``oss://`` for DashScope).\n \"\"\"\n return get_provider().upload_file(api_key, model, fp)\n\n\ndef resolve_file(\n value: str,\n *,\n api_key: str | None = None,\n model: str | None = None,\n) -> str:\n \"\"\"Resolve a file reference for API consumption.\n\n URLs (``http``, ``https``, ``data``, and provider-managed schemes)\n pass through unchanged. Local files are handled based on context:\n\n - *api_key* + *model* provided: upload to temp storage.\n - Otherwise: convert to ``data:`` base64 URI (must be \u003c 7 MB).\n \"\"\"\n provider = get_provider()\n pass_through = (\"http://\", \"https://\", \"data:\") + provider.managed_url_schemes()\n if value.startswith(pass_through):\n return value\n if value.startswith(\"file://\"):\n parsed = urllib.parse.urlparse(value)\n if parsed.netloc and parsed.netloc.lower() != \"localhost\":\n raise ValueError(\n f\"Cannot resolve non-local file URI authority {parsed.netloc!r}; \"\n \"only an empty authority or 'localhost' is supported.\"\n )\n value = urllib.request.url2pathname(parsed.path)\n p = Path(value)\n if not (p.exists() and p.is_file()):\n return value\n\n file_size = p.stat().st_size\n\n if api_key and model:\n managed_url = provider.upload_file(api_key, model, p)\n tag = \"48 h TTL\" if managed_url.startswith(\"oss://\") else \"custom OSS\"\n print(sanitize_diagnostic(f\"Uploaded {p.name} -> {managed_url} ({tag})\"), file=sys.stderr)\n return managed_url\n\n if file_size > _BASE64_FILE_LIMIT:\n print(\n f\"Warning: {p.name} is {file_size / 1024 / 1024:.1f} MB -- \"\n \"base64 may exceed the 10 MB API limit. \"\n \"Use --upload-files to auto-upload, or provide an online URL.\",\n file=sys.stderr,\n )\n\n mime = mimetypes.guess_type(p.name)[0] or \"application/octet-stream\"\n b64 = base64.b64encode(p.read_bytes()).decode(\"ascii\")\n return f\"data:{mime};base64,{b64}\"\n\n\ndef download_file(url: str, dest: Path, *, timeout: int = 120) -> Path:\n \"\"\"Download a file from *url* to *dest*, creating parent dirs as needed.\"\"\"\n dest.parent.mkdir(parents=True, exist_ok=True)\n req = urllib.request.Request(url, headers={\"User-Agent\": \"qwencloud-skills\"})\n try:\n with urllib.request.urlopen(req, timeout=timeout) as resp:\n dest.write_bytes(resp.read())\n except urllib.error.URLError as exc:\n raise RuntimeError(f\"Download failed: {sanitize_diagnostic(str(exc))}\") from None\n return dest\n\n\n# ---------------------------------------------------------------------------\n# Section 8: Request / response utilities\n# ---------------------------------------------------------------------------\n\ndef load_request(args: Any) -> dict[str, Any]:\n \"\"\"Load request dict from ``--request`` or ``--file`` CLI argument.\"\"\"\n if getattr(args, \"request\", None):\n return json.loads(args.request)\n if getattr(args, \"file\", None):\n return json.loads(Path(args.file).read_text(encoding=\"utf-8\"))\n raise ValueError(\"Provide --request '{...}' or --file path/to/request.json\")\n\n\ndef save_result(result: dict[str, Any], output_path: str | Path) -> None:\n \"\"\"Write *result* as JSON to *output_path*, creating parent dirs.\"\"\"\n p = Path(output_path)\n p.parent.mkdir(parents=True, exist_ok=True)\n p.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n\n\ndef extract_text(content: Any) -> str:\n \"\"\"Extract plain text from a message ``content`` field.\n\n Handles ``str``, ``list[{type, text}]``, and ``None``.\n \"\"\"\n if isinstance(content, str):\n return content\n if isinstance(content, list):\n for item in content:\n if isinstance(item, dict) and item.get(\"type\") == \"text\":\n text = item.get(\"text\")\n if isinstance(text, str):\n return text\n return json.dumps(content, ensure_ascii=False)\n if content is None:\n return \"\"\n return json.dumps(content, ensure_ascii=False)\n\n\ndef try_parse_json(text: str) -> Any | None:\n \"\"\"Attempt to parse *text* as JSON; return ``None`` on failure.\"\"\"\n text = text.strip()\n if not text:\n return None\n try:\n return json.loads(text)\n except json.JSONDecodeError:\n return None\n\n\n# ---------------------------------------------------------------------------\n# Section 9: Async task polling\n# ---------------------------------------------------------------------------\n\ndef poll_task(\n task_id: str,\n api_key: str,\n *,\n timeout_s: int = 600,\n interval: int = 10,\n verbose: bool = True,\n) -> dict[str, Any]:\n \"\"\"Poll an async task until it reaches a terminal state.\n\n Returns the final result dict. Raises ``TimeoutError`` if\n *timeout_s* is exceeded.\n \"\"\"\n provider = get_provider()\n url = provider.task_poll_url(task_id)\n terminals = provider.terminal_statuses()\n start = time.time()\n poll_count = 0\n while True:\n result = http_request(\"GET\", url, api_key)\n elapsed = int(time.time() - start)\n poll_count += 1\n status = provider.extract_task_status(result)\n if verbose:\n print(f\" [{elapsed}s] task={task_id} status={status}\", file=sys.stderr)\n if status in terminals:\n return result\n if time.time() - start > timeout_s:\n raise TimeoutError(\n f\"Task {task_id} timed out after {timeout_s}s \"\n f\"({poll_count} polls, last status: {status})\"\n )\n time.sleep(interval)\n\n\n# ---------------------------------------------------------------------------\n# Section 10: Update-check signal\n# ---------------------------------------------------------------------------\n\ntry:\n from gossamer import run as _run_update_signal_impl\nexcept ImportError:\n _run_update_signal_impl = None # type: ignore[assignment]\n\n\ndef run_update_signal(caller: str | Path | None = None) -> None:\n \"\"\"Emit update-check signals to stderr (non-blocking, failure-safe).\n\n Parameters\n ----------\n caller : str or Path, optional\n ``__file__`` of the calling script.\n \"\"\"\n if _run_update_signal_impl:\n try:\n _run_update_signal_impl(caller=caller or __file__)\n except Exception:\n pass\n","content_type":"text/x-python; charset=utf-8","language":"python","size":49931,"content_sha256":"fadc836540173f9a5b1dd531f3b266931e181b2cf94c1d5bbae892c15469ed33"},{"filename":"scripts/tts_cosyvoice.py","content":"#!/usr/bin/env python3\n\"\"\"Synthesize speech from text via CosyVoice models (WebSocket API).\n\nCosyVoice models require the DashScope SDK (WebSocket-based, not HTTP REST).\nRun with --help for usage.\n\nDependencies:\n pip install dashscope>=1.25.17\n\nOr with venv:\n python3 -m venv .venv && source .venv/bin/activate && pip install dashscope>=1.25.17\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\n\nif sys.version_info \u003c (3, 9):\n print(f\"Error: Python 3.9+ required (found {sys.version}).\", file=sys.stderr)\n sys.exit(1)\n\n# Check dashscope dependency before other imports\ntry:\n import dashscope\n from dashscope.audio.tts_v2 import SpeechSynthesizer\n from dashscope.audio.tts_v2.speech_synthesizer import AudioFormat\nexcept ImportError:\n print(\n \"Error: dashscope SDK not installed.\\n\\n\"\n \"Install with:\\n\"\n \" pip install dashscope>=1.25.17\\n\\n\"\n \"Or use venv:\\n\"\n \" python3 -m venv .venv\\n\"\n \" source .venv/bin/activate # Windows: .venv\\\\Scripts\\\\activate\\n\"\n \" pip install dashscope>=1.25.17\",\n file=sys.stderr,\n )\n sys.exit(1)\n\nimport argparse\nimport json\nimport urllib.parse\nfrom pathlib import Path\nfrom typing import Any\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\n\nfrom qwencloud_lib import ( # noqa: E402\n build_source_config,\n load_cdn_model_config,\n native_base_url,\n require_api_key,\n run_update_signal,\n)\n\n# is_token_plan_key is being added to qwencloud_lib; fall back to a local\n# check until the shared helper is exported in every installation.\ntry:\n from qwencloud_lib import is_token_plan_key # noqa: E402,F401\nexcept ImportError:\n def is_token_plan_key(api_key: str) -> bool: # type: ignore[misc]\n return api_key.startswith(\"sk-sp-\")\n\n# ---------------------------------------------------------------------------\n# Constants\n# ---------------------------------------------------------------------------\n\nSKILL_USER_AGENT = \"qwencloud-skills\" # Required header for Token Plan routing\n_MODEL_CONFIG_DIR = Path(__file__).resolve().parent.parent / \"cdn\" / \"config\"\n\n# The WebSocket endpoint is derived at runtime from the shared native base\n# URL (see qwencloud_lib), so a custom QWEN_BASE_URL override and Token Plan\n# routing apply to the WebSocket path automatically. With no override,\n# native_base_url() defaults to https://dashscope-intl.aliyuncs.com/api/v1,\n# resolving to wss://dashscope-intl.aliyuncs.com/api-ws/v1/inference.\n_WEBSOCKET_PATH = \"/api-ws/v1/inference\"\n\ndef _validate_model_config(config: dict[str, Any]) -> bool:\n cosyvoice = config.get(\"cosyvoice\")\n if not isinstance(cosyvoice, dict):\n return False\n model_voices = cosyvoice.get(\"model_voices\")\n instruction_voices = cosyvoice.get(\"instruction_voices\")\n if not (\n isinstance(cosyvoice.get(\"default_model\"), str)\n and bool(cosyvoice[\"default_model\"])\n and isinstance(cosyvoice.get(\"default_voice\"), str)\n and bool(cosyvoice[\"default_voice\"])\n and isinstance(model_voices, dict)\n and bool(model_voices)\n and all(\n isinstance(model, str)\n and model\n and isinstance(voices, list)\n and bool(voices)\n and all(isinstance(voice, str) and voice for voice in voices)\n for model, voices in model_voices.items()\n )\n and isinstance(instruction_voices, dict)\n and all(\n isinstance(model, str)\n and model in model_voices\n and isinstance(voices, list)\n and all(isinstance(voice, str) and voice for voice in voices)\n and set(voices).issubset(model_voices[model])\n for model, voices in instruction_voices.items()\n )\n ):\n return False\n default_model = cosyvoice[\"default_model\"]\n return (\n default_model in model_voices\n and cosyvoice[\"default_voice\"] in model_voices[default_model]\n )\n\n\ndef _model_config() -> dict[str, Any]:\n return load_cdn_model_config(\n \"qwencloud-audio-tts-config.json\",\n local_dir=_MODEL_CONFIG_DIR,\n required_keys=(\"cosyvoice\",),\n validator=_validate_model_config,\n )[\"cosyvoice\"]\n\n\ndef _default_model() -> str:\n return _model_config()[\"default_model\"]\n\n\ndef _default_voice() -> str:\n return _model_config()[\"default_voice\"]\n\n\ndef _voice_map(key: str) -> dict[str, frozenset[str]]:\n return {\n model: frozenset(voices)\n for model, voices in _model_config()[key].items()\n }\n\n_SAMPLE_RATES = (8000, 16000, 22050, 24000, 44100, 48000)\n\n# (format, sample_rate) -> SDK AudioFormat enum for the WebSocket synthesizer\n_AUDIO_FORMATS = {\n (\"mp3\", 8000): AudioFormat.MP3_8000HZ_MONO_128KBPS,\n (\"mp3\", 16000): AudioFormat.MP3_16000HZ_MONO_128KBPS,\n (\"mp3\", 22050): AudioFormat.MP3_22050HZ_MONO_256KBPS,\n (\"mp3\", 24000): AudioFormat.MP3_24000HZ_MONO_256KBPS,\n (\"mp3\", 44100): AudioFormat.MP3_44100HZ_MONO_256KBPS,\n (\"mp3\", 48000): AudioFormat.MP3_48000HZ_MONO_256KBPS,\n (\"wav\", 8000): AudioFormat.WAV_8000HZ_MONO_16BIT,\n (\"wav\", 16000): AudioFormat.WAV_16000HZ_MONO_16BIT,\n (\"wav\", 22050): AudioFormat.WAV_22050HZ_MONO_16BIT,\n (\"wav\", 24000): AudioFormat.WAV_24000HZ_MONO_16BIT,\n (\"wav\", 44100): AudioFormat.WAV_44100HZ_MONO_16BIT,\n (\"wav\", 48000): AudioFormat.WAV_48000HZ_MONO_16BIT,\n (\"pcm\", 8000): AudioFormat.PCM_8000HZ_MONO_16BIT,\n (\"pcm\", 16000): AudioFormat.PCM_16000HZ_MONO_16BIT,\n (\"pcm\", 22050): AudioFormat.PCM_22050HZ_MONO_16BIT,\n (\"pcm\", 24000): AudioFormat.PCM_24000HZ_MONO_16BIT,\n (\"pcm\", 44100): AudioFormat.PCM_44100HZ_MONO_16BIT,\n (\"pcm\", 48000): AudioFormat.PCM_48000HZ_MONO_16BIT,\n}\n\n\ndef _ws_url() -> str:\n \"\"\"Derive the WebSocket inference endpoint from the shared native base URL.\n\n The ws/wss scheme mirrors the base URL's http/https scheme and the host\n is preserved, so QWEN_BASE_URL overrides and Token Plan routing (both\n handled inside ``native_base_url()``) apply automatically.\n \"\"\"\n parsed = urllib.parse.urlparse(native_base_url())\n scheme = \"wss\" if parsed.scheme == \"https\" else \"ws\"\n return f\"{scheme}://{parsed.netloc}{_WEBSOCKET_PATH}\"\n\n\ndef _resolve_audio_format(fmt: str, sample_rate: int) -> AudioFormat:\n try:\n return _AUDIO_FORMATS[(fmt, sample_rate)]\n except KeyError:\n print(\n f\"Error: unsupported format/sample-rate combination: \"\n f\"{fmt}@{sample_rate}Hz. \"\n f\"Sample rates: {', '.join(str(r) for r in _SAMPLE_RATES)}.\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main() -> None:\n run_update_signal(caller=__file__)\n\n parser = argparse.ArgumentParser(\n description=\"CosyVoice TTS via DashScope SDK (WebSocket)\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=f\"\"\"\\\nmodel examples (current compatibility/defaults load from CDN configuration):\n cosyvoice-v3-flash High quality, fast, supports system voices\n cosyvoice-v3-plus Highest quality, supports system voices\n\nvoice compatibility:\n cosyvoice-v3-plus longanyang, longanhuan\n cosyvoice-v3-flash See CDN model configuration/catalog for the full list\n\ninstruction compatibility:\n The selected model and voice must both be instruction-compatible. The script\n validates the exact pair against CDN configuration before making a request.\n\nnote:\n CosyVoice models are NOT available on Token Plan. With a Token Plan key\n (sk-sp-...), use tts.py with qwen-audio-3.0-tts-plus instead.\n\n For Qwen TTS models (qwen3-tts-*, qwen-audio-3.0-tts-plus), use tts.py.\n\nexamples:\n # Basic synthesis\n python {Path(__file__).name} --text \"Hello, world!\"\n\n # Chinese with specific voice\n python {Path(__file__).name} --text \"你好世界\" --voice longanhuan\n\n # High quality model\n python {Path(__file__).name} --text \"Hello\" --model cosyvoice-v3-plus\n\n # Instruction-guided style control (cosyvoice-v3-flash)\n python {Path(__file__).name} --text \"欢迎光临\" --instruction \"用热情洋溢的声音\" --language-hints zh\n\n # WAV output at 48 kHz\n python {Path(__file__).name} --text \"Hello\" --format wav --sample-rate 48000\n\n # Save to specific file\n python {Path(__file__).name} --text \"Hello\" --output hello.mp3\n\"\"\",\n )\n parser.add_argument(\"--text\", \"-t\", required=True, help=\"Text to synthesize\")\n parser.add_argument(\"--model\", \"-m\", default=None,\n help=\"Model (default loaded from CDN model configuration)\")\n parser.add_argument(\"--voice\", \"-v\", default=None,\n help=\"Voice (default loaded from CDN model configuration)\")\n parser.add_argument(\"--output\", \"-o\", type=Path, default=Path(\"output/qwencloud-audio-tts/cosyvoice.mp3\"), help=\"Output file (default: output/qwencloud-audio-tts/cosyvoice.mp3)\")\n parser.add_argument(\"--format\", \"-f\", default=\"mp3\", choices=[\"mp3\", \"wav\", \"pcm\"], help=\"Audio format (default: mp3)\")\n parser.add_argument(\"--sample-rate\", type=int, default=24000, choices=_SAMPLE_RATES,\n help=\"Sample rate in Hz (default: 24000)\")\n parser.add_argument(\"--instruction\", type=str, default=None,\n help=\"Free-style speech instruction; supported voices are loaded from CDN configuration\")\n parser.add_argument(\"--language-hints\", type=str, default=None,\n help=\"Target language hint (e.g. zh, en)\")\n args = parser.parse_args()\n\n api_key = require_api_key(script_file=__file__, domain=\"CosyVoice TTS\")\n if is_token_plan_key(api_key):\n print(\n \"Error: cosyvoice models are not available on Token Plan.\\n\"\n \"Use tts.py with qwen-audio-3.0-tts-plus instead (Token Plan supported).\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n # Model priority: CLI > default\n try:\n model = args.model or _default_model()\n voice = args.voice or _default_voice()\n model_voices = _voice_map(\"model_voices\")\n instruction_voices = _voice_map(\"instruction_voices\")\n except RuntimeError as exc:\n print(f\"Error: {exc}\", file=sys.stderr)\n sys.exit(1)\n\n if model not in model_voices:\n print(\n f\"Error: unsupported CosyVoice model '{model}'. Supported models: \"\n f\"{', '.join(sorted(model_voices))}\",\n file=sys.stderr,\n )\n sys.exit(1)\n if voice not in model_voices[model]:\n print(\n f\"Error: voice '{voice}' is not supported by model '{model}'. \"\n f\"Supported voices: {', '.join(sorted(model_voices[model]))}\",\n file=sys.stderr,\n )\n sys.exit(1)\n if args.instruction and voice not in instruction_voices.get(model, frozenset()):\n supported = \", \".join(sorted(instruction_voices.get(model, frozenset()))) or \"none\"\n print(\n f\"Error: --instruction is not supported for voice '{voice}' with model \"\n f\"'{model}'. Instruction-compatible voices: {supported}\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n # Setup: derive the WebSocket endpoint AFTER require_api_key so that\n # Token Plan / custom QWEN_BASE_URL routing inside native_base_url() is\n # already in effect.\n dashscope.api_key = api_key\n websocket_url = _ws_url()\n dashscope.base_websocket_api_url = websocket_url\n\n # Synthesize\n print(f\"Synthesizing (WebSocket): model={model}, voice={voice}\", file=sys.stderr)\n try:\n headers = {\"User-Agent\": SKILL_USER_AGENT}\n source_config = build_source_config(__file__)\n if source_config:\n headers[\"X-DashScope-Source-Config\"] = source_config\n synthesizer = SpeechSynthesizer(\n model=model,\n voice=voice,\n format=_resolve_audio_format(args.format, args.sample_rate),\n instruction=args.instruction or None,\n language_hints=[args.language_hints] if args.language_hints else None,\n headers=headers,\n url=websocket_url,\n )\n audio_data = synthesizer.call(args.text)\n except Exception as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n if not audio_data:\n # Structured WebSocket error extraction: pull error_code /\n # error_message / task_id from the last task-failed response header.\n try:\n response = synthesizer.get_response()\n except Exception:\n response = None\n header = response.get(\"header\", {}) if isinstance(response, dict) else {}\n if header.get(\"event\") == \"task-failed\":\n error_code = header.get(\"error_code\", \"UnknownError\")\n error_message = header.get(\"error_message\", \"Unknown error\")\n task_id = header.get(\"task_id\", \"\")\n suffix = f\" (task_id={task_id})\" if task_id else \"\"\n print(\n f\"Error: WebSocket task failed: {error_code}: \"\n f\"{error_message}{suffix}\",\n file=sys.stderr,\n )\n else:\n print(\"Error: No audio data returned.\", file=sys.stderr)\n sys.exit(1)\n\n # Save\n output = args.output\n if output.suffix.lower() not in {\".mp3\", \".wav\", \".pcm\"}:\n output = output.with_suffix(f\".{args.format}\")\n output.parent.mkdir(parents=True, exist_ok=True)\n output.write_bytes(audio_data)\n\n print(f\"Audio saved to {output}\", file=sys.stderr)\n print(json.dumps({\"audio_file\": str(output), \"size_bytes\": len(audio_data)}))\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":13685,"content_sha256":"784232538e3218734efa67d9febb8900c6e8b081f1c083a62e99b345188fe0bd"},{"filename":"scripts/tts.py","content":"#!/usr/bin/env python3\n\"\"\"Synthesize speech from text via Qwen TTS models on DashScope API.\n\nSupports multiple voices, instruction-controlled style, and automatic audio\ndownload. Self-contained, stdlib only.\n\nProtocol routing:\n - qwen3-tts-flash, qwen3-tts-instruct-flash → HTTP REST API\n - qwen-audio-3.0-tts-plus/flash → raw WebSocket duplex streaming\n - cosyvoice-v3-* → scripts/tts_cosyvoice.py (DashScope SDK)\n\"\"\"\nfrom __future__ import annotations\n\nimport sys\n\nif sys.version_info \u003c (3, 9):\n print(f\"Error: Python 3.9+ required (found {sys.version}). \"\n \"Install: https://www.python.org/downloads/\", file=sys.stderr)\n sys.exit(1)\n\nimport argparse\nimport hashlib\nimport json\nimport os\nimport socket\nimport ssl\nimport struct\nimport time\nimport uuid as uuid_mod\nfrom pathlib import Path\nfrom typing import Any\nfrom urllib.parse import urlparse\n\nsys.path.insert(0, str(Path(__file__).resolve().parent))\n\nfrom qwencloud_lib import ( # noqa: E402\n check_token_plan_model_support,\n download_file,\n http_request,\n load_cdn_model_config,\n load_request,\n native_base_url,\n require_api_key,\n run_update_signal,\n)\n\n# ---------------------------------------------------------------------------\n# TTS constants\n# ---------------------------------------------------------------------------\n\nTTS_GENERATION_PATH = \"/services/aigc/multimodal-generation/generation\"\n_AUDIO_EXTS = {\".wav\", \".mp3\", \".flac\", \".ogg\", \".pcm\", \".aac\", \".opus\"}\n\n_MODEL_CONFIG_DIR = Path(__file__).resolve().parent.parent / \"cdn\" / \"config\"\n\n\ndef _validate_model_config(config: dict[str, Any]) -> bool:\n tts = config.get(\"tts\")\n if not isinstance(tts, dict):\n return False\n cosyvoice = config.get(\"cosyvoice\")\n cosyvoice_models = (\n set(cosyvoice.get(\"model_voices\", {}))\n if isinstance(cosyvoice, dict) and isinstance(cosyvoice.get(\"model_voices\"), dict)\n else set()\n )\n voices = tts.get(\"ws_default_voices\")\n if not (\n isinstance(tts.get(\"default_model\"), str)\n and bool(tts[\"default_model\"])\n and isinstance(tts.get(\"default_voice\"), str)\n and bool(tts[\"default_voice\"])\n and isinstance(tts.get(\"ws_models\"), list)\n and all(isinstance(item, str) and item for item in tts[\"ws_models\"])\n and isinstance(voices, dict)\n and all(\n isinstance(model, str) and isinstance(voice, str) and voice\n for model, voice in voices.items()\n )\n ):\n return False\n ws_models = set(tts[\"ws_models\"])\n return (\n tts[\"default_model\"] in ws_models\n and ws_models == set(voices)\n and ws_models.isdisjoint(cosyvoice_models)\n )\n\n\ndef _model_config() -> dict[str, Any]:\n return load_cdn_model_config(\n \"qwencloud-audio-tts-config.json\",\n local_dir=_MODEL_CONFIG_DIR,\n required_keys=(\"tts\",),\n validator=_validate_model_config,\n )[\"tts\"]\n\n\ndef _default_model() -> str:\n return _model_config()[\"default_model\"]\n\n\ndef _default_voice() -> str:\n return _model_config()[\"default_voice\"]\n\n\ndef _ws_models() -> frozenset[str]:\n return frozenset(_model_config()[\"ws_models\"])\n\n\ndef _ws_default_voices() -> dict[str, str]:\n return dict(_model_config()[\"ws_default_voices\"])\n\n# WebSocket endpoint paths\n_WS_PATH = \"/api-ws/v1/inference\"\n_WS_HOST_PAYG = \"dashscope-intl.aliyuncs.com\"\n_WS_HOST_TOKEN_PLAN = \"token-plan.ap-southeast-1.maas.aliyuncs.com\"\n\n\ndef _is_audio_file_output(path: Path) -> bool:\n \"\"\"Return whether *path* denotes an audio file rather than a directory.\"\"\"\n return not path.is_dir() and path.suffix.lower() in _AUDIO_EXTS\n\n\n# ---------------------------------------------------------------------------\n# WebSocket minimal implementation (stdlib: ssl + socket + struct + hashlib)\n# ---------------------------------------------------------------------------\n\ndef _ws_connect(host: str, path: str, api_key: str) -> socket.socket:\n \"\"\"Perform WebSocket HTTP Upgrade handshake, return connected socket.\"\"\"\n ctx = ssl.create_default_context()\n raw = socket.create_connection((host, 443), timeout=60)\n sock = ctx.wrap_socket(raw, server_hostname=host)\n\n ws_key = hashlib.sha1(uuid_mod.uuid4().bytes).digest()\n import base64\n ws_key_b64 = base64.b64encode(ws_key[:16]).decode()\n\n handshake = (\n f\"GET {path} HTTP/1.1\\r\\n\"\n f\"Host: {host}\\r\\n\"\n f\"Upgrade: websocket\\r\\n\"\n f\"Connection: Upgrade\\r\\n\"\n f\"Sec-WebSocket-Key: {ws_key_b64}\\r\\n\"\n f\"Sec-WebSocket-Version: 13\\r\\n\"\n f\"Authorization: Bearer {api_key}\\r\\n\"\n f\"User-Agent: qwencloud-skills\\r\\n\"\n f\"\\r\\n\"\n )\n sock.sendall(handshake.encode())\n\n # Read HTTP response\n resp_data = b\"\"\n while b\"\\r\\n\\r\\n\" not in resp_data:\n chunk = sock.recv(4096)\n if not chunk:\n raise RuntimeError(\"WebSocket handshake failed: connection closed\")\n resp_data += chunk\n\n status_line = resp_data.split(b\"\\r\\n\", 1)[0].decode()\n if \"101\" not in status_line:\n raise RuntimeError(f\"WebSocket handshake failed: {status_line}\")\n\n return sock\n\n\ndef _ws_send_text(sock: socket.socket, data: str) -> None:\n \"\"\"Send a masked text frame over WebSocket.\"\"\"\n payload = data.encode(\"utf-8\")\n length = len(payload)\n\n # Build frame header: FIN=1, opcode=1 (text), MASK=1\n header = bytearray()\n header.append(0x81) # FIN + text opcode\n\n if length \u003c 126:\n header.append(0x80 | length)\n elif length \u003c 65536:\n header.append(0x80 | 126)\n header.extend(struct.pack(\"!H\", length))\n else:\n header.append(0x80 | 127)\n header.extend(struct.pack(\"!Q\", length))\n\n # Masking key (4 random bytes)\n mask = os.urandom(4)\n header.extend(mask)\n\n # Mask the payload\n masked = bytearray(length)\n for i in range(length):\n masked[i] = payload[i] ^ mask[i % 4]\n\n sock.sendall(bytes(header) + bytes(masked))\n\n\ndef _ws_recv_frame(sock: socket.socket) -> tuple[int, bytes]:\n \"\"\"Receive one complete WebSocket message. Returns (opcode, payload).\"\"\"\n def _recv_exact(n: int) -> bytes:\n buf = b\"\"\n while len(buf) \u003c n:\n chunk = sock.recv(n - len(buf))\n if not chunk:\n raise RuntimeError(\"WebSocket connection closed unexpectedly\")\n buf += chunk\n return buf\n\n def _recv_raw_frame() -> tuple[bool, int, bytes]:\n hdr = _recv_exact(2)\n fin = bool(hdr[0] & 0x80)\n opcode = hdr[0] & 0x0F\n masked = bool(hdr[1] & 0x80)\n length = hdr[1] & 0x7F\n\n if opcode >= 0x08:\n if not fin:\n raise RuntimeError(\"WebSocket protocol error: control frame must not be fragmented\")\n if length > 125:\n raise RuntimeError(\n \"WebSocket protocol error: control frame payload exceeds 125 bytes\"\n )\n\n if length == 126:\n length = struct.unpack(\"!H\", _recv_exact(2))[0]\n elif length == 127:\n length = struct.unpack(\"!Q\", _recv_exact(8))[0]\n\n mask_key = _recv_exact(4) if masked else None\n payload = _recv_exact(length)\n\n if mask_key:\n payload = bytes(payload[i] ^ mask_key[i % 4] for i in range(length))\n\n return fin, opcode, payload\n\n message_opcode: int | None = None\n message_chunks: list[bytes] = []\n\n while True:\n fin, opcode, payload = _recv_raw_frame()\n\n if opcode == 0x09: # Ping control frame may interrupt a fragmented message\n _ws_send_pong(sock, payload)\n continue\n if opcode == 0x0A: # Pong control frame\n continue\n if opcode == 0x08: # Close control frame\n return opcode, payload\n\n if opcode in (0x01, 0x02):\n if message_opcode is not None:\n raise RuntimeError(\"WebSocket protocol error: new data frame before continuation\")\n if fin:\n return opcode, payload\n message_opcode = opcode\n message_chunks.append(payload)\n continue\n\n if opcode == 0x00:\n if message_opcode is None:\n raise RuntimeError(\"WebSocket protocol error: unexpected continuation frame\")\n message_chunks.append(payload)\n if fin:\n return message_opcode, b\"\".join(message_chunks)\n continue\n\n raise RuntimeError(f\"WebSocket protocol error: unsupported opcode {opcode}\")\n\n\ndef _ws_close(sock: socket.socket) -> None:\n \"\"\"Send close frame and shut down.\"\"\"\n try:\n # Send close frame: FIN=1, opcode=8, masked, empty payload\n mask = os.urandom(4)\n sock.sendall(b\"\\x88\\x80\" + mask)\n except OSError:\n pass\n try:\n sock.shutdown(socket.SHUT_RDWR)\n except OSError:\n pass\n sock.close()\n\n\ndef _ws_send_pong(sock: socket.socket, payload: bytes) -> None:\n \"\"\"Send a masked pong frame with the given payload.\"\"\"\n pong_hdr = bytearray([0x8A]) # FIN + pong opcode\n plen = len(payload)\n if plen \u003c 126:\n pong_hdr.append(0x80 | plen)\n else:\n pong_hdr.append(0x80 | 126)\n pong_hdr.extend(struct.pack(\"!H\", plen))\n mask = os.urandom(4)\n pong_hdr.extend(mask)\n masked_payload = bytes(payload[i] ^ mask[i % 4] for i in range(plen))\n sock.sendall(bytes(pong_hdr) + masked_payload)\n\n\ndef _ws_tts(api_key: str, model: str, text: str, voice: str,\n output: Path, *, sample_rate: int = 24000,\n audio_format: str = \"mp3\",\n style_params: dict[str, Any] | None = None,\n use_model_default_voice: bool = False) -> dict[str, Any]:\n \"\"\"Perform TTS via WebSocket duplex protocol.\n\n *style_params* may carry optional run-task style parameters\n (``volume`` / ``rate`` / ``pitch``); only keys explicitly provided\n by the user are forwarded.\n\n Returns dict with audio_file path and metadata.\n \"\"\"\n # Determine endpoint based on key type\n if api_key.startswith(\"sk-sp-\"):\n host = _WS_HOST_TOKEN_PLAN\n else:\n host = _WS_HOST_PAYG\n\n # Resolve the model default only when no voice was supplied by the caller.\n # An explicit voice named \"Cherry\" must remain Cherry even though that is\n # also the generic CLI default.\n ws_default_voices = _ws_default_voices()\n if use_model_default_voice and model in ws_default_voices:\n voice = ws_default_voices[model]\n print(f\"Using default voice '{voice}' for model {model}\", file=sys.stderr)\n\n task_id = str(uuid_mod.uuid4())\n\n print(f\"Connecting to wss://{host}{_WS_PATH} ...\", file=sys.stderr)\n sock = _ws_connect(host, _WS_PATH, api_key)\n\n try:\n # 1. Send run-task\n ws_params: dict[str, Any] = {\n \"text_type\": \"PlainText\",\n \"format\": audio_format,\n \"sample_rate\": sample_rate,\n \"voice\": voice,\n }\n for k in (\"volume\", \"rate\", \"pitch\"):\n if style_params and style_params.get(k) is not None:\n ws_params[k] = style_params[k]\n\n run_task_msg = json.dumps({\n \"header\": {\n \"action\": \"run-task\",\n \"task_id\": task_id,\n \"streaming\": \"duplex\",\n },\n \"payload\": {\n \"task_group\": \"audio\",\n \"task\": \"tts\",\n \"function\": \"SpeechSynthesizer\",\n \"model\": model,\n \"parameters\": ws_params,\n \"input\": {},\n },\n })\n _ws_send_text(sock, run_task_msg)\n\n # 2. Wait for task-started event before sending text (protocol requirement)\n task_started = False\n error_msg = \"\"\n while not task_started:\n opcode, frame_payload = _ws_recv_frame(sock)\n if opcode == 0x01: # Text frame\n try:\n event = json.loads(frame_payload.decode(\"utf-8\"))\n except (json.JSONDecodeError, UnicodeDecodeError):\n continue\n header = event.get(\"header\", {})\n event_name = header.get(\"event\", \"\")\n if event_name == \"task-started\":\n task_started = True\n elif event_name == \"task-failed\":\n error_msg = header.get(\"error_message\", \"Unknown error\")\n raise RuntimeError(f\"WebSocket TTS task-start failed: {error_msg}\")\n elif opcode == 0x08: # Close frame\n raise RuntimeError(\"WebSocket closed before task-started\")\n\n # 3. Send continue-task with text\n continue_task_msg = json.dumps({\n \"header\": {\n \"action\": \"continue-task\",\n \"task_id\": task_id,\n \"streaming\": \"duplex\",\n },\n \"payload\": {\n \"input\": {\"text\": text},\n },\n })\n _ws_send_text(sock, continue_task_msg)\n\n # 4. Send finish-task\n finish_task_msg = json.dumps({\n \"header\": {\n \"action\": \"finish-task\",\n \"task_id\": task_id,\n \"streaming\": \"duplex\",\n },\n \"payload\": {\n \"input\": {},\n },\n })\n _ws_send_text(sock, finish_task_msg)\n\n # 5. Receive frames: binary = audio data, text = event JSON\n audio_chunks: list[bytes] = []\n task_completed = False\n error_msg = \"\"\n\n while not task_completed:\n opcode, frame_payload = _ws_recv_frame(sock)\n\n if opcode == 0x02: # Binary frame → audio data\n audio_chunks.append(frame_payload)\n elif opcode == 0x01: # Text frame → event JSON\n try:\n event = json.loads(frame_payload.decode(\"utf-8\"))\n except (json.JSONDecodeError, UnicodeDecodeError):\n continue\n\n header = event.get(\"header\", {})\n event_name = header.get(\"event\", \"\")\n\n if event_name == \"task-finished\":\n task_completed = True\n elif event_name == \"task-failed\":\n error_msg = header.get(\"error_message\") or \"Unknown error\"\n raise RuntimeError(f\"WebSocket TTS failed: {error_msg}\")\n elif opcode == 0x08: # Close frame\n raise RuntimeError(\"WebSocket TTS closed before task-finished\")\n\n finally:\n _ws_close(sock)\n\n if error_msg:\n raise RuntimeError(f\"WebSocket TTS failed: {error_msg}\")\n\n if not audio_chunks:\n raise RuntimeError(\"WebSocket TTS: no audio data received\")\n\n # Concatenate and write audio. A recognized audio suffix means the caller\n # supplied an exact file path; otherwise the path is an output directory.\n if _is_audio_file_output(output):\n audio_file = output\n output_dir = output.parent\n else:\n output_dir = output\n ext = f\".{audio_format}\" if not audio_format.startswith(\".\") else audio_format\n ts = time.strftime(\"%Y%m%d-%H%M%S\")\n audio_file = output_dir / f\"audio-{ts}{ext}\"\n\n output_dir.mkdir(parents=True, exist_ok=True)\n audio_file.write_bytes(b\"\".join(audio_chunks))\n\n total_bytes = sum(len(c) for c in audio_chunks)\n print(f\"WebSocket TTS complete: {total_bytes} bytes received\", file=sys.stderr)\n print(f\"Audio saved to {audio_file}\", file=sys.stderr)\n\n return {\n \"audio_file\": str(audio_file),\n \"model\": model,\n \"format\": audio_format,\n \"sample_rate\": sample_rate,\n \"bytes\": total_bytes,\n }\n\n\n# ---------------------------------------------------------------------------\n# Helpers\n# ---------------------------------------------------------------------------\n\ndef _guess_audio_ext(url: str, default: str = \".wav\") -> str:\n \"\"\"Extract audio extension from URL path, ignoring query params.\"\"\"\n path = urlparse(url).path\n for ext in (\".wav\", \".mp3\", \".flac\", \".ogg\", \".pcm\", \".aac\"):\n if path.endswith(ext):\n return ext\n return default\n\n\n# ---------------------------------------------------------------------------\n# HTTP REST TTS (qwen3-tts-flash, qwen3-tts-instruct-flash)\n# ---------------------------------------------------------------------------\n\ndef _http_tts(api_key: str, model: str, voice: str, text: str,\n request: dict[str, Any], args: Any) -> None:\n \"\"\"Perform TTS via HTTP REST API.\"\"\"\n input_obj: dict[str, Any] = {\"text\": text, \"voice\": voice}\n if request.get(\"language_type\"):\n input_obj[\"language_type\"] = request[\"language_type\"]\n\n payload: dict[str, Any] = {\"model\": model, \"input\": input_obj}\n\n if request.get(\"instructions\") and \"instruct\" in model.lower():\n input_obj[\"instructions\"] = request[\"instructions\"]\n if request.get(\"optimize_instructions\") is not None:\n input_obj[\"optimize_instructions\"] = request[\"optimize_instructions\"]\n\n url = f\"{native_base_url()}{TTS_GENERATION_PATH}\"\n\n try:\n resp = http_request(\"POST\", url, api_key, payload, timeout=60)\n except RuntimeError as e:\n print(f\"API error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n audio_url = (resp.get(\"output\") or {}).get(\"audio\", {}).get(\"url\")\n if not audio_url:\n print(f\"Error: No audio URL in response: {resp}\", file=sys.stderr)\n sys.exit(1)\n\n out = args.output\n\n if out.suffix.lower() in _AUDIO_EXTS:\n audio_file = out\n out_dir = out.parent\n else:\n out_dir = out\n ext = _guess_audio_ext(audio_url)\n ts = time.strftime(\"%Y%m%d-%H%M%S\")\n audio_file = out_dir / f\"audio-{ts}{ext}\"\n\n out_dir.mkdir(parents=True, exist_ok=True)\n\n resp_file = out_dir / \"response.json\"\n resp_file.write_text(json.dumps(resp, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n print(f\"Response saved to {resp_file}\", file=sys.stderr)\n\n try:\n download_file(audio_url, audio_file)\n except Exception as e:\n print(f\"Warning: Could not download audio: {e}\", file=sys.stderr)\n print(f\"Audio URL (manual download): {audio_url}\", file=sys.stderr)\n else:\n print(f\"Audio saved to {audio_file}\", file=sys.stderr)\n\n if args.print_response:\n print(json.dumps({\"audio_url\": audio_url, \"audio_file\": str(audio_file)}, ensure_ascii=False))\n\n\n# ---------------------------------------------------------------------------\n# Main\n# ---------------------------------------------------------------------------\n\ndef main() -> None:\n run_update_signal(caller=__file__)\n parser = argparse.ArgumentParser(\n description=\"Synthesize speech from text via Qwen TTS\",\n formatter_class=argparse.RawDescriptionHelpFormatter,\n epilog=\"\"\"\\\nrequest JSON fields (--request / --file):\n text (required) Text to synthesize into speech\n voice Voice ID — overridden by --voice flag; defaults and\n WebSocket model mappings load from CDN configuration\n model Model ID — overridden by --model flag\n (default loaded from CDN configuration)\n language_type Force language: \"Auto\" / \"Chinese\" / \"English\" / \"Japanese\" /\n \"Korean\" etc. (full names, not \"zh\"/\"en\" codes).\n HTTP models only — WebSocket models ignore this field\n (their language follows the selected voice)\n instructions Style instructions for instruct models, e.g.\n \"Speak slowly with a warm, gentle tone\"\n optimize_instructions true/false — auto-optimize instruction text\n sample_rate Audio sample rate in Hz (WebSocket models, default 24000)\n format Audio format: mp3/wav/pcm/opus (WebSocket models, default mp3)\n volume Volume [0-100], default 50 (WebSocket models only)\n rate Speech rate [0.5-2.0], default 1.0; below 1.0 slows\n speech, above speeds up (WebSocket models only)\n pitch Pitch multiplier [0.5-2.0], default 1.0; above 1.0 =\n higher pitch, below = lower (WebSocket models only)\n\nmodel examples (current routing/defaults load from CDN configuration):\n qwen-audio-3.0-tts-plus Next-gen TTS, higher quality,\n WebSocket streaming (PAYG + Token Plan)\n qwen3-tts-flash Non-streaming HTTP path — fast, multi-voice\n qwen3-tts-instruct-flash Instruction-controlled style (non-streaming HTTP)\n\n Note: CosyVoice models (cosyvoice-v3-plus/flash) require WebSocket API.\n Use tts_cosyvoice.py instead of this script.\n\n Token Plan (sk-sp-...) keys: only qwen-audio-3.0-tts-plus is supported.\n For basic style control on Token Plan, use volume/rate/pitch with\n qwen-audio-3.0-tts-plus (instruct models require a PAYG key).\n\noutput:\n --output can be a directory (audio saved as audio.wav/mp3 inside)\n or a specific file path (e.g. output/speech.mp3). Extension is\n auto-detected from the API response URL.\n\nenvironment variables:\n QWENCLOUD_API_KEY (preferred) API key — also loaded from .env\n QWEN_API_KEY (fallback) Legacy alias\n DASHSCOPE_API_KEY (fallback) Legacy provider variable\n QWEN_REGION ap-southeast-1 (default)\n\nexamples:\n # Simple TTS\n python scripts/tts.py --request '{\"text\":\"Hello, world!\"}'\n\n # Chinese text with specific voice\n python scripts/tts.py --request '{\"text\":\"你好世界\"}' --voice Chelsie\n\n # Instruction-controlled style\n python scripts/tts.py --request '{\"text\":\"Breaking news today...\",\n \"instructions\":\"Speak in a serious news anchor tone, moderate pace\"}' \\\\\n --model qwen3-tts-instruct-flash\n\n # Save to specific file\n python scripts/tts.py --request '{\"text\":\"Hello\"}' --output output/hello.mp3\n\n # Style control via WebSocket model (default; works with PAYG + Token Plan keys)\n python scripts/tts.py --request '{\"text\":\"落霞与孤鹜齐飞\",\n \"rate\":0.8,\"pitch\":1.1,\"volume\":60}'\n\n # HTTP fallback model (low cost)\n python scripts/tts.py --request '{\"text\":\"Hello\"}' --model qwen3-tts-flash\n\"\"\",\n )\n parser.add_argument(\"--request\", type=str,\n help=\"Inline JSON: must contain 'text' field\")\n parser.add_argument(\"--file\", type=Path,\n help=\"Path to JSON file containing request body\")\n parser.add_argument(\"--output\", type=Path, default=Path(\"output/qwencloud-audio-tts\"),\n help=\"Output directory or file path (default: %(default)s)\")\n parser.add_argument(\"--print-response\", action=\"store_true\",\n help=\"Print audio URL and file path JSON to stdout\")\n parser.add_argument(\"--model\", type=str, default=None,\n help=\"Model ID (default loaded from CDN model configuration)\")\n parser.add_argument(\"--voice\", type=str, default=None,\n help=\"Voice ID (default loaded from CDN model configuration)\")\n args = parser.parse_args()\n\n try:\n request = load_request(args)\n except ValueError as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n text = request.get(\"text\")\n if not text:\n print(\"Error: text is required.\", file=sys.stderr)\n sys.exit(1)\n\n requested_model = args.model if args.model is not None else request.get(\"model\")\n if isinstance(requested_model, str) and requested_model.startswith(\"cosyvoice-\"):\n print(\n f\"Error: {requested_model} is not supported by tts.py. \"\n \"Use scripts/tts_cosyvoice.py for CosyVoice models.\",\n file=sys.stderr,\n )\n sys.exit(1)\n\n api_key = require_api_key(script_file=__file__, domain=\"TTS\")\n try:\n # CLI flags intentionally override values loaded from request JSON.\n voice_explicit = args.voice is not None or bool(request.get(\"voice\"))\n voice = args.voice if args.voice is not None else request.get(\"voice\") or _default_voice()\n model = requested_model or _default_model()\n check_token_plan_model_support(model, domain=\"TTS\")\n use_websocket = model in _ws_models()\n except RuntimeError as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # --- Route by protocol: WebSocket models vs HTTP models ---\n if use_websocket:\n # Raw WebSocket path for Qwen-Audio TTS models only.\n out = args.output\n if _is_audio_file_output(out):\n out_dir = out.parent\n else:\n out_dir = out\n\n sample_rate = request.get(\"sample_rate\", 24000)\n audio_format = request.get(\"format\", \"mp3\")\n # Optional style control (WebSocket models only) — forward only if\n # explicitly provided by the user. This gives Token Plan users basic\n # style control as an alternative to instruct models' instructions.\n style_params = {\n k: request[k] for k in (\"volume\", \"rate\", \"pitch\")\n if request.get(k) is not None\n }\n\n try:\n result = _ws_tts(\n api_key, model, text, voice, out,\n sample_rate=sample_rate, audio_format=audio_format,\n style_params=style_params,\n use_model_default_voice=not voice_explicit,\n )\n except RuntimeError as e:\n print(f\"Error: {e}\", file=sys.stderr)\n sys.exit(1)\n\n # Save response JSON\n resp_file = out_dir / \"response.json\"\n resp_file.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding=\"utf-8\")\n print(f\"Response saved to {resp_file}\", file=sys.stderr)\n\n if args.print_response:\n print(json.dumps(result, ensure_ascii=False))\n\n else:\n # HTTP REST path for qwen3-tts-flash, qwen3-tts-instruct-flash\n _http_tts(api_key, model, voice, text, request, args)\n\n\nif __name__ == \"__main__\":\n main()\n","content_type":"text/x-python; charset=utf-8","language":"python","size":25953,"content_sha256":"46a5c8d96ad02facf0458ff9eb202f2a8271b10ddae5c5c07333908ec794efe3"}],"content_json":{"type":"doc","content":[{"type":"heading","attrs":{"level":1},"content":[{"text":"Qwen Audio TTS (Text-to-Speech)","type":"text"}]},{"type":"paragraph","content":[{"text":"Synthesize natural speech from text using Qwen TTS models. This skill is part of ","type":"text"},{"text":"qwencloud/qwencloud-ai","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Skill directory","type":"text"}]},{"type":"paragraph","content":[{"text":"Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details.","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Location","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Purpose","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/tts.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Qwen TTS / Qwen Audio TTS (HTTP and WebSocket); current compatibility and defaults are in the model catalog below","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"scripts/tts_cosyvoice.py","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CosyVoice (WebSocket API) — requires ","type":"text"},{"text":"dashscope","type":"text","marks":[{"type":"code_inline"}]},{"text":" SDK; current compatibility and defaults are in the model catalog below","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/cosyvoice-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"CosyVoice setup, voices, examples, errors","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/execution-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Fallback: curl (standard, instruct, streaming), code generation","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/prompt-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Text formatting for speech, instructions templates, voice selection","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/api-guide.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"API supplement","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"references/sources.md","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Official documentation URLs","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Security","type":"text"}]},{"type":"paragraph","content":[{"text":"NEVER output any API key or credential in plaintext.","type":"text","marks":[{"type":"strong"}]},{"text":" Always use variable references (","type":"text"},{"text":"$QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" in shell, ","type":"text"},{"text":"os.environ[\"QWENCLOUD_API_KEY\"]","type":"text","marks":[{"type":"code_inline"}]},{"text":" in Python). The scripts accept ","type":"text"},{"text":"QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"QWEN_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":". Any check or detection of credentials must be ","type":"text"},{"text":"non-plaintext","type":"text","marks":[{"type":"strong"}]},{"text":": report only status (e.g. \"set\" / \"not set\", \"valid\" / \"invalid\"), never the value. Never display contents of ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" or config files that may contain secrets.","type":"text"}]},{"type":"paragraph","content":[{"text":"When the API key is not configured, NEVER ask the user to provide it directly.","type":"text","marks":[{"type":"strong"}]},{"text":" Instead, help create a ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file with a placeholder (","type":"text"},{"text":"QWENCLOUD_API_KEY=sk-your-key-here","type":"text","marks":[{"type":"code_inline"}]},{"text":") and instruct the user to replace it with their actual key from the ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":". Only write the actual key value if the user explicitly requests it.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Key Compatibility","type":"text"}]},{"type":"paragraph","content":[{"text":"Scripts support both ","type":"text"},{"text":"standard QwenCloud API keys","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"sk-...","type":"text","marks":[{"type":"code_inline"}]},{"text":") and ","type":"text"},{"text":"Token Plan keys","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"sk-sp-...","type":"text","marks":[{"type":"code_inline"}]},{"text":"). Token Plan keys are automatically routed to the Token Plan endpoint for supported TTS models — see ","type":"text"},{"text":"Token Plan Support","type":"text","marks":[{"type":"link","attrs":{"href":"#token-plan-support","title":null}}]},{"text":" below.","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan: do not use curl; always use the bundled Python scripts.","type":"text","marks":[{"type":"strong"}]}]},{"type":"paragraph","content":[{"text":"Coding Plan keys (also ","type":"text"},{"text":"sk-sp-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix but purchased via Coding Plan subscription) cannot be used — TTS models are not available on Coding Plan. The script detects key type at startup and routes accordingly. If qwencloud-ops-auth is installed, see its ","type":"text"},{"text":"references/codingplan.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" for full details.","type":"text"}]},{"type":"paragraph","content":[{"text":"Detect the API key type without exposing the key:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 -c \"\nimport sys; sys.path.insert(0, 'scripts')\nfrom qwencloud_lib import detect_api_key_type\nprint(detect_api_key_type('scripts/qwencloud_lib.py'))\n\"","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Output","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Meaning","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"token-plan","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token Plan key detected (","type":"text"},{"text":"sk-sp-","type":"text","marks":[{"type":"code_inline"}]},{"text":" prefix)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"payg","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Standard PAYG key detected","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"not-set","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"No API key found in environment","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"For Token Plan, fetch and read the current ","type":"text"},{"text":"Token Plan model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-token-plan-models.md","title":null}}]},{"text":", then use an exact listed model with ","type":"text"},{"text":"scripts/tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":". If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-token-plan-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Model Selection","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"🚫 CRITICAL — Never override the user's model choice.","type":"text","marks":[{"type":"strong"}]},{"text":" If the user (or the request JSON) explicitly specifies a ","type":"text"},{"text":"model","type":"text","marks":[{"type":"code_inline"}]},{"text":", you MUST use exactly that model. Do NOT:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Replace it with a \"better suited\" model (e.g., switching to ","type":"text"},{"text":"qwen3-tts-instruct-flash","type":"text","marks":[{"type":"code_inline"}]},{"text":" for stylized/poetic text)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Add ","type":"text"},{"text":"instructions","type":"text","marks":[{"type":"code_inline"}]},{"text":" the user did not ask for","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Modify any parameter the user explicitly provided","type":"text"}]}]}]},{"type":"paragraph","content":[{"text":"Model selection guidance below applies ","type":"text"},{"text":"only when the user has NOT specified a model","type":"text","marks":[{"type":"strong"}]},{"text":".","type":"text"}]}]},{"type":"paragraph","content":[{"text":"Before selecting, recommending, or defaulting a model, fetch and read the current ","type":"text"},{"text":"QwenCloud audio TTS model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-audio-tts-models.md","title":null}}]},{"text":". It contains the model list, basic model information, script and voice compatibility, recommendations, and defaults. If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-audio-tts-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Consult the ","type":"text"},{"text":"qwencloud-model-selector","type":"text","marks":[{"type":"strong"}]},{"text":" skill when model choice depends on capability, scenario, or pricing. CosyVoice requires the ","type":"text"},{"text":"dashscope","type":"text","marks":[{"type":"code_inline"}]},{"text":" SDK and uses different voices; see ","type":"text"},{"text":"cosyvoice-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cosyvoice-guide.md","title":null}}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Important","type":"text","marks":[{"type":"strong"}]},{"text":": The model catalog is a ","type":"text"},{"text":"point-in-time snapshot","type":"text","marks":[{"type":"strong"}]},{"text":" and may be outdated. Model availability changes frequently. ","type":"text"},{"text":"Always check the ","type":"text","marks":[{"type":"strong"}]},{"text":"official model list","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.qwencloud.com/models","title":null}},{"type":"strong"}]},{"text":" for the authoritative, up-to-date catalog before making model decisions.","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Model details","type":"text","marks":[{"type":"strong"}]},{"text":": For more information about a specific model, direct the user to ","type":"text"},{"text":"https://www.qwencloud.com/models/\u003cmodel-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":". Replace ","type":"text"},{"text":"\u003cmodel-name>","type":"text","marks":[{"type":"code_inline"}]},{"text":" with the exact model ID; never modify or guess it.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Dynamic model queries","type":"text","marks":[{"type":"strong"}]},{"text":": If the ","type":"text"},{"text":"qwencloud-model-selector","type":"text","marks":[{"type":"strong"}]},{"text":" skill or ","type":"text"},{"text":"QwenCloud CLI","type":"text","marks":[{"type":"strong"}]},{"text":" (","type":"text"},{"text":"qwencloud models info \u003cmodel>","type":"text","marks":[{"type":"code_inline"}]},{"text":") is available, use it for real-time model data. CLI requires authentication — see the ","type":"text"},{"text":"qwencloud-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill for login flow.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Available Voices","type":"text"}]},{"type":"paragraph","content":[{"text":"Use the QwenCloud audio TTS model catalog linked above for current model-to-voice compatibility and defaults. For complete voice inventories, use the official voice-list links in ","type":"text"},{"text":"sources.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sources.md","title":null}}]},{"text":".","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Voice defaulting","type":"text","marks":[{"type":"strong"}]},{"text":": When no voice is supplied for a Qwen-Audio WebSocket model, ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" selects that model's configured default voice and prints a notice to stderr. Any explicit voice—including explicit ","type":"text"},{"text":"Cherry","type":"text","marks":[{"type":"code_inline"}]},{"text":"—is passed through unchanged. Fetch the model catalog before choosing a voice.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Execution","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"⚠️ Multiple artifacts","type":"text","marks":[{"type":"strong"}]},{"text":": When generating multiple files in a single session, you MUST append a numeric suffix to each filename (e.g. ","type":"text"},{"text":"out_1.wav","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"out_2.wav","type":"text","marks":[{"type":"code_inline"}]},{"text":") to prevent overwrites.","type":"text"}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Qwen TTS (HTTP API) — ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Prerequisites","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"API Key","type":"text","marks":[{"type":"strong"}]},{"text":": Check ","type":"text"},{"text":"QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"QWEN_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":", then ","type":"text"},{"text":"DASHSCOPE_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" using a ","type":"text"},{"text":"non-plaintext","type":"text","marks":[{"type":"strong"}]},{"text":" check only (e.g. in shell: ","type":"text"},{"text":"[ -n \"$QWENCLOUD_API_KEY\" ]","type":"text","marks":[{"type":"code_inline"}]},{"text":"; report only \"set\" or \"not set\", never the key value). If not set: run the ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" skill if available; otherwise guide the user to obtain a key from ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":" and set it via ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" file (","type":"text"},{"text":"echo 'QWENCLOUD_API_KEY=sk-your-key-here' >> .env","type":"text","marks":[{"type":"code_inline"}]},{"text":" in project root or current directory) or environment variable. The script searches for ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the current working directory and the project root. Skills may be installed independently — do not assume qwencloud-ops-auth is present. ","type":"text"},{"text":"Note","type":"text","marks":[{"type":"strong"}]},{"text":": The script auto-loads ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":" from the current directory and the project root (in addition to any exported environment variable). A shell check showing ","type":"text"},{"text":"$QWENCLOUD_API_KEY","type":"text","marks":[{"type":"code_inline"}]},{"text":" as \"not set\" does NOT mean the script will fail — it may still find the key in ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":". Treat the shell check as informational only; the authoritative test is simply running the script (it exits with a clear error if no key is found anywhere).","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Python 3.9+ (stdlib only, ","type":"text"},{"text":"no pip install needed","type":"text","marks":[{"type":"strong"}]},{"text":")","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Environment Check","type":"text"}]},{"type":"paragraph","content":[{"text":"Before first execution, verify Python is available:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 --version # must be 3.9+","type":"text"}]},{"type":"paragraph","content":[{"text":"If ","type":"text"},{"text":"python3","type":"text","marks":[{"type":"code_inline"}]},{"text":" is unavailable or below 3.9, PAYG may use ","type":"text"},{"text":"Path 2 (curl)","type":"text","marks":[{"type":"strong"}]},{"text":"; Token Plan must install Python 3.9+ instead.","type":"text"}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Default: Run Script","type":"text"}]},{"type":"paragraph","content":[{"text":"Script path","type":"text","marks":[{"type":"strong"}]},{"text":": Scripts are in the ","type":"text"},{"text":"scripts/","type":"text","marks":[{"type":"code_inline"}]},{"text":" subdirectory ","type":"text"},{"text":"of this skill's directory","type":"text","marks":[{"type":"strong"}]},{"text":" (the directory containing this SKILL.md). ","type":"text"},{"text":"You MUST first locate this skill's installation directory, then ALWAYS use the full absolute path to execute scripts.","type":"text","marks":[{"type":"strong"}]},{"text":" Do NOT assume scripts are in the current working directory. Do NOT use ","type":"text"},{"text":"cd","type":"text","marks":[{"type":"code_inline"}]},{"text":" to switch directories before execution.","type":"text"}]},{"type":"paragraph","content":[{"text":"Execution note:","type":"text","marks":[{"type":"strong"}]},{"text":" Run all scripts in the ","type":"text"},{"text":"foreground","type":"text","marks":[{"type":"strong"}]},{"text":" — wait for stdout; do not background.","type":"text"}]},{"type":"paragraph","content":[{"text":"Discovery:","type":"text","marks":[{"type":"strong"}]},{"text":" Run ","type":"text"},{"text":"python3 \u003cthis-skill-dir>/scripts/tts.py --help","type":"text","marks":[{"type":"code_inline"}]},{"text":" first to see all available arguments.","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"python3 \u003cthis-skill-dir>/scripts/tts.py \\\n --request '{\"text\":\"Hello, this is a test.\",\"voice\":\"Cherry\"}' \\\n --output output/qwencloud-audio-tts/ \\\n --print-response","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Argument","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--request '{...}'","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"JSON request body","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--file path.json","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Load request from file","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--output path","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Save audio and response JSON to directory, or specify an audio file path (e.g. ","type":"text"},{"text":"speech.mp3","type":"text","marks":[{"type":"code_inline"}]},{"text":"); use distinct filenames across calls to avoid overwriting","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--print-response","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Print response to stdout","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--model ID","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Override model","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"--voice NAME","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Override voice","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"Verify Result","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Exit code ","type":"text"},{"text":"0","type":"text","marks":[{"type":"code_inline"}]},{"text":" + output contains valid JSON with ","type":"text"},{"text":"output.audio","type":"text","marks":[{"type":"code_inline"}]},{"text":" field → ","type":"text"},{"text":"success","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Non-zero exit, HTTP error, empty response, or error JSON → ","type":"text"},{"text":"fail","type":"text","marks":[{"type":"strong"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Post-execution check","type":"text","marks":[{"type":"strong"}]},{"text":": Verify the output audio file exists and has non-zero size (","type":"text"},{"text":"ls -la \u003coutput_dir>","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"MANDATORY — stderr signal check","type":"text","marks":[{"type":"strong"}]},{"text":": After confirming the result, scan the command's stderr output for ","type":"text"},{"text":"[ACTION_REQUIRED]","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]},{"text":". If either signal is present, you ","type":"text"},{"text":"MUST","type":"text","marks":[{"type":"strong"}]},{"text":" follow the instructions in ","type":"text"},{"text":"Update Check","type":"text","marks":[{"type":"link","attrs":{"href":"#update-check-mandatory-post-execution","title":null}}]},{"text":" below before responding to the user.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":4},"content":[{"text":"On Failure","type":"text"}]},{"type":"paragraph","content":[{"text":"If the script fails, match the error output against the diagnostic table below to determine the resolution. If no match, read ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":" for alternative paths: curl commands (Path 2 — standard, instruct, streaming), code generation (Path 3), and autonomous resolution (Path 5).","type":"text"}]},{"type":"paragraph","content":[{"text":"If Python is not available at all","type":"text","marks":[{"type":"strong"}]},{"text":" → PAYG may use Path 2 (curl); Token Plan must install Python 3.9+.","type":"text"}]},{"type":"hr","attrs":{"markup":"---"}},{"type":"heading","attrs":{"level":3},"content":[{"text":"CosyVoice — ","type":"text"},{"text":"tts_cosyvoice.py","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"CosyVoice requires ","type":"text"},{"text":"dashscope","type":"text","marks":[{"type":"code_inline"}]},{"text":" SDK. Quick start:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"bash"},"content":[{"text":"pip install dashscope>=1.25.17\npython3 \u003cthis-skill-dir>/scripts/tts_cosyvoice.py --text \"Hello\"","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Token Plan","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"tts_cosyvoice.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" rejects Token Plan keys at startup. Fetch the Token Plan catalog linked above and use an exact supported TTS model with ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":", or use a PAYG key. Never replace an explicitly requested model without the user's confirmation.","type":"text"}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Full guide","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"cosyvoice-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/cosyvoice-guide.md","title":null}}]},{"text":" (setup, voices, examples, errors)","type":"text"}]}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error Pattern","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Diagnosis","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Resolution","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"command not found: python3","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python not on PATH","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Try ","type":"text"},{"text":"python","type":"text","marks":[{"type":"code_inline"}]},{"text":" or ","type":"text"},{"text":"py -3","type":"text","marks":[{"type":"code_inline"}]},{"text":"; install Python 3.9+ if missing","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python 3.9+ required","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Script version check failed","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upgrade Python to 3.9+","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SyntaxError","type":"text","marks":[{"type":"code_inline"}]},{"text":" near type hints","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Python \u003c 3.9","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Upgrade Python to 3.9+","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"cosyvoice models are not available on Token Plan","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Token Plan key detected","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" with an exact supported model from the Token Plan catalog, or a PAYG key","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"QWENCLOUD_API_KEY/QWEN_API_KEY/DASHSCOPE_API_KEY not found","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Missing API key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Obtain key from ","type":"text"},{"text":"QwenCloud Console","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/api-keys","title":null}}]},{"text":"; add to ","type":"text"},{"text":".env","type":"text","marks":[{"type":"code_inline"}]},{"text":": ","type":"text"},{"text":"echo 'QWENCLOUD_API_KEY=sk-...' >> .env","type":"text","marks":[{"type":"code_inline"}]},{"text":"; or run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" if available","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 401","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid or mismatched key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" (non-plaintext check only); verify key is valid","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SSL: CERTIFICATE_VERIFY_FAILED","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"SSL cert issue (proxy/corporate)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"macOS: run ","type":"text"},{"text":"Install Certificates.command","type":"text","marks":[{"type":"code_inline"}]},{"text":"; else set ","type":"text"},{"text":"SSL_CERT_FILE","type":"text","marks":[{"type":"code_inline"}]},{"text":" env var","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URLError","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"ConnectionError","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Network unreachable","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Check internet; set ","type":"text"},{"text":"HTTPS_PROXY","type":"text","marks":[{"type":"code_inline"}]},{"text":" if behind proxy","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 429","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limited","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Wait and retry with backoff","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"HTTP 5xx","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Server error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retry with backoff","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"PermissionError","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Can't write output","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"--output","type":"text","marks":[{"type":"code_inline"}]},{"text":" to specify writable directory","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Quick Reference","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Request Fields","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Type","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Required","type":"text","marks":[{"type":"strong"}]},{"text":" — text to synthesize; check the model catalog above for model-specific limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"voice","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Voice ID; check the model catalog above for current defaults and compatibility","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"model","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Model ID; check the model catalog above for the current default","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"language_type","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Auto","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Chinese","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"English","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Japanese","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Korean","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"French","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"German","type":"text","marks":[{"type":"code_inline"}]},{"text":", etc. — full names, not codes (","type":"text"},{"text":"zh","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":")","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"instructions","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Tone/style instructions; check the model catalog above for current compatibility and limits","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"volume","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"int","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Volume ","type":"text"},{"text":"[0-100]","type":"text","marks":[{"type":"code_inline"}]},{"text":", default 50 — supported WebSocket models only; check the model catalog","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"rate","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"float","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Speech rate ","type":"text"},{"text":"[0.5-2.0]","type":"text","marks":[{"type":"code_inline"}]},{"text":", default 1.0; below 1.0 slows speech — ","type":"text"},{"text":"WebSocket models only","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"pitch","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"float","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Pitch multiplier ","type":"text"},{"text":"[0.5-2.0]","type":"text","marks":[{"type":"code_inline"}]},{"text":", default 1.0; above 1.0 raises pitch — ","type":"text"},{"text":"WebSocket models only","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sample_rate","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"int","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sample rate in Hz, default 24000 — ","type":"text"},{"text":"WebSocket models only","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"format","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"string","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Audio format: ","type":"text"},{"text":"mp3","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"wav","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"pcm","type":"text","marks":[{"type":"code_inline"}]},{"text":"/","type":"text"},{"text":"opus","type":"text","marks":[{"type":"code_inline"}]},{"text":", default ","type":"text"},{"text":"mp3","type":"text","marks":[{"type":"code_inline"}]},{"text":" — ","type":"text"},{"text":"WebSocket models only","type":"text","marks":[{"type":"strong"}]}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"stream","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"bool","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Direct Qwen3-TTS HTTP API field for SSE streaming. Bundled ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" does not implement this field; use the direct SSE path in ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]}]}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Response Fields","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Field","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Description","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"audio_url","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"URL of generated audio (valid 24h)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"audio_format","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Format (e.g. wav)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"sample_rate","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Sample rate (e.g. 24000)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"usage","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Character usage","type":"text"}]}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Important Notes","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"text","type":"text","marks":[{"type":"strong"}]},{"text":": Check the model catalog above for the selected model's current request limit.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"instructions","type":"text","marks":[{"type":"strong"}]},{"text":": Support is model-specific; check the model catalog above before using it.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"language_type","type":"text","marks":[{"type":"strong"}]},{"text":": ","type":"text"},{"text":"Auto","type":"text","marks":[{"type":"code_inline"}]},{"text":" for mixed language; specify for better pronunciation. Values are full names (","type":"text"},{"text":"Chinese","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"English","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"Japanese","type":"text","marks":[{"type":"code_inline"}]},{"text":", ...), not codes (","type":"text"},{"text":"zh","type":"text","marks":[{"type":"code_inline"}]},{"text":", ","type":"text"},{"text":"en","type":"text","marks":[{"type":"code_inline"}]},{"text":"). It applies only to compatible HTTP models; the script ignores it for WebSocket models whose language follows the selected voice.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"audio_url","type":"text","marks":[{"type":"strong"}]},{"text":": Valid for 24 hours — download promptly.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Real-time/streaming TTS","type":"text","marks":[{"type":"strong"}]},{"text":": Bundled ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" uses non-streaming HTTP for Qwen3-TTS and raw WebSocket only for Qwen-Audio TTS; CosyVoice is handled by ","type":"text"},{"text":"tts_cosyvoice.py","type":"text","marks":[{"type":"code_inline"}]},{"text":". The Qwen3-TTS API itself supports optional SSE; use the direct example in ","type":"text"},{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":" when needed.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Cross-Skill Chaining","type":"text"}]},{"type":"paragraph","content":[{"text":"When passing generated audio to another skill (e.g., video-gen audio overlay):","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Pass ","type":"text","marks":[{"type":"strong"}]},{"text":"audio_url","type":"text","marks":[{"type":"code_inline"},{"type":"strong"}]},{"text":" directly","type":"text","marks":[{"type":"strong"}]},{"text":" — scripts detect URL prefixes and pass through without re-upload","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Use ","type":"text"},{"text":"audio_file","type":"text","marks":[{"type":"code_inline"}]},{"text":" only for local playback or non-API operations","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Error Handling","type":"text"}]},{"type":"table","attrs":{"layout":null},"content":[{"type":"tr","content":[{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Error","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Cause","type":"text"}]}]},{"type":"th","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Action","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"401 Unauthorized","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid or missing API key","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Run ","type":"text"},{"text":"qwencloud-ops-auth","type":"text","marks":[{"type":"strong"}]},{"text":" if available; else prompt user to set key (non-plaintext check only)","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"400","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Invalid parameters (missing text/voice)","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Validate request body","type":"text"}]}]}]},{"type":"tr","content":[{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"429","type":"text","marks":[{"type":"code_inline"}]},{"text":" / ","type":"text"},{"text":"5xx","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Rate limit or server error","type":"text"}]}]},{"type":"td","attrs":{"colspan":1,"rowspan":1,"colwidth":null,"alignment":""},"content":[{"type":"paragraph","content":[{"text":"Retry with backoff","type":"text"}]}]}]}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"Usage & billing","type":"text","marks":[{"type":"strong"}]},{"text":": Use the ","type":"text"},{"text":"qwencloud-usage","type":"text","marks":[{"type":"strong"}]},{"text":" skill to check usage, free tier quota, and billing directly. Alternatively, the user can visit the QwenCloud console: ","type":"text"},{"text":"Usage Analytics","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/analytics","title":null}}]},{"text":" | ","type":"text"},{"text":"Pay-as-you-go Billing","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/billing/pay-as-you-go","title":null}}]},{"text":" | ","type":"text"},{"text":"Coding Plan Billing","type":"text","marks":[{"type":"link","attrs":{"href":"https://home.qwencloud.com/billing/coding-plan","title":null}}]}]},{"type":"paragraph","content":[{"text":"For the current pricing model list and billing units, fetch the ","type":"text"},{"text":"CDN model-pricing reference","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-model-pricing.md","title":null}}]},{"text":". If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-model-pricing.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"NEVER fabricate, guess, or construct usage/billing/console URLs.","type":"text","marks":[{"type":"strong"}]},{"text":" Only provide the exact links listed in this skill. If a URL is not listed here, do not invent one.","type":"text"}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Output Location","type":"text"}]},{"type":"paragraph","content":[{"text":"Prefer the ","type":"text"},{"text":"current working directory","type":"text","marks":[{"type":"strong"}]},{"text":". Default subdirectory: ","type":"text"},{"text":"./output/qwencloud-audio-tts/","type":"text","marks":[{"type":"code_inline"}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"Write prohibition","type":"text","marks":[{"type":"strong"}]},{"text":": NEVER write output files into this skill's installation directory or any ","type":"text"},{"text":"skills/","type":"text","marks":[{"type":"code_inline"}]},{"text":" hierarchy. All generated content must go to ","type":"text"},{"text":"output/","type":"text","marks":[{"type":"code_inline"}]},{"text":" under the current working directory or a user-specified path.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Token Plan Support","type":"text"}]},{"type":"paragraph","content":[{"text":"Token Plan supports only a subset of TTS models. Fetch and read the current ","type":"text"},{"text":"Token Plan model catalog","type":"text","marks":[{"type":"link","attrs":{"href":"https://alioth-intl.alicdn.com/skills-info/models/references/qwencloud-token-plan-models.md","title":null}}]},{"text":" before selecting or validating a model. If CDN access fails, use the ","type":"text"},{"text":"local fallback","type":"text","marks":[{"type":"link","attrs":{"href":"cdn/references/qwencloud-token-plan-models.md","title":null}}]},{"text":".","type":"text"}]},{"type":"paragraph","content":[{"text":"When a Token Plan key is combined with an unsupported model, ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" prints a warning to stderr listing the currently supported models and suggesting alternatives; it does not block the call. If you see this warning, tell the user which models the current catalog supports and offer either a catalog-listed alternative or a PAYG key for the original model. Never silently replace an explicitly requested model.","type":"text"}]},{"type":"paragraph","content":[{"text":"For CosyVoice specifically, ","type":"text"},{"text":"tts_cosyvoice.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" ","type":"text"},{"text":"hard-blocks","type":"text","marks":[{"type":"strong"}]},{"text":" Token Plan keys at startup. Use ","type":"text"},{"text":"tts.py","type":"text","marks":[{"type":"code_inline"}]},{"text":" with a model listed in the current Token Plan catalog, or use a PAYG key.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Style control on Token Plan","type":"text"}]},{"type":"paragraph","content":[{"text":"Use only style-control fields supported by the selected Token Plan model in the audio model catalog. The following is a protocol example for a currently supported model; do not substitute it for a user-specified model:","type":"text"}]},{"type":"code_block","attrs":{"wrap":false,"language":"json"},"content":[{"text":"{\"text\": \"落霞与孤鹜齐飞,秋水共长天一色。\", \"model\": \"qwen-audio-3.0-tts-plus\", \"rate\": 0.8, \"pitch\": 1.1, \"volume\": 60}","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"rate","type":"text","marks":[{"type":"code_inline"}]},{"text":" below 1.0 slows speech, above 1.0 speeds it up","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"pitch","type":"text","marks":[{"type":"code_inline"}]},{"text":" above 1.0 raises pitch, below 1.0 lowers it","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"volume","type":"text","marks":[{"type":"code_inline"}]},{"text":" scales linearly (0 = silent, 50 = default, 100 = max)","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Protocol note","type":"text"}]},{"type":"paragraph","content":[{"text":"The ","type":"text"},{"text":"QwenCloud model page","type":"text","marks":[{"type":"link","attrs":{"href":"https://www.qwencloud.com/models/qwen-audio-3.0-tts-plus","title":null}}]},{"text":" documents ","type":"text"},{"text":"WSS (WebSocket)","type":"text","marks":[{"type":"strong"}]},{"text":" for ","type":"text"},{"text":"qwen-audio-3.0-tts-plus","type":"text","marks":[{"type":"code_inline"}]},{"text":". The script includes a WebSocket path branch that:","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Connects to ","type":"text"},{"text":"wss://token-plan.ap-southeast-1.maas.aliyuncs.com/api-ws/v1/inference","type":"text","marks":[{"type":"code_inline"}]},{"text":" (or standard endpoint for PAYG)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sets ","type":"text"},{"text":"User-Agent: qwencloud-skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" in the WebSocket handshake","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Sends run-task → continue-task → finish-task messages","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Receives binary audio frames and concatenates them into the output file","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Required header","type":"text"}]},{"type":"paragraph","content":[{"text":"User-Agent: qwencloud-skills","type":"text","marks":[{"type":"code_inline"}]},{"text":" is automatically included in both HTTP and WebSocket requests.","type":"text"}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"Update Check (MANDATORY Post-Execution)","type":"text"}]},{"type":"paragraph","content":[{"text":"After script execution, check stderr for action signals. The script automatically detects update status and emits structured signals. You MUST act on them. This section also applies when the user explicitly asks to check for updates.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Signal: ","type":"text"},{"text":"[ACTION_REQUIRED] qwencloud-update-check skill is not installed","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"When stderr contains this signal, you MUST present the following options to the user and ","type":"text"},{"text":"wait for their response before proceeding","type":"text","marks":[{"type":"strong"}]},{"text":". Do NOT silently skip this.","type":"text"}]},{"type":"blockquote","content":[{"type":"paragraph","content":[{"text":"The update-check skill is not installed. It keeps you informed about new qwencloud/qwencloud-ai features and bug fixes.","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Install now","type":"text","marks":[{"type":"strong"}]},{"text":" — I'll run the install command for you","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Skip for now","type":"text","marks":[{"type":"strong"}]},{"text":" — do nothing this time","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Never remind me","type":"text","marks":[{"type":"strong"}]},{"text":" — permanently suppress this prompt","type":"text"}]}]}]}]},{"type":"paragraph","content":[{"text":"After the user responds:","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"1","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"npx skills add QwenCloud/qwencloud-ai --skill qwencloud-update-check -y","type":"text","marks":[{"type":"code_inline"}]},{"text":", then run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --dismiss","type":"text","marks":[{"type":"code_inline"}]}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"2","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --dismiss","type":"text","marks":[{"type":"code_inline"}]},{"text":", then continue normally","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"User chooses ","type":"text"},{"text":"3","type":"text","marks":[{"type":"strong"}]},{"text":" → run: ","type":"text"},{"text":"python3 \u003cthis-skill-scripts-dir>/gossamer.py --never-install","type":"text","marks":[{"type":"code_inline"}]},{"text":", then continue normally","type":"text"}]}]}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Signal: ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]}]},{"type":"paragraph","content":[{"text":"When stderr contains this signal, you MUST append a brief update notice to your response, including the version info and the update command shown in the stderr output.","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"No signal in stderr","type":"text"}]},{"type":"paragraph","content":[{"text":"If stderr contains neither ","type":"text"},{"text":"[ACTION_REQUIRED]","type":"text","marks":[{"type":"code_inline"}]},{"text":" nor ","type":"text"},{"text":"[UPDATE_AVAILABLE]","type":"text","marks":[{"type":"code_inline"}]},{"text":", no action is needed — the skill is installed and up to date (or cached within 24h).","type":"text"}]},{"type":"heading","attrs":{"level":3},"content":[{"text":"Explicit user request","type":"text"}]},{"type":"paragraph","content":[{"text":"When the user explicitly asks to check for updates (e.g. \"check for updates\", \"check version\"):","type":"text"}]},{"type":"ordered_list","attrs":{"order":1,"listStyle":"number"},"content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"Look for ","type":"text"},{"text":"qwencloud-update-check/SKILL.md","type":"text","marks":[{"type":"code_inline"}]},{"text":" in sibling skill directories.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If found — run: ","type":"text"},{"text":"python3 \u003cqwencloud-update-check-dir>/scripts/check_update.py --print-response","type":"text","marks":[{"type":"code_inline"}]},{"text":" and report the result.","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"If not found — present the install options above.","type":"text"}]}]}]},{"type":"heading","attrs":{"level":2},"content":[{"text":"References","type":"text"}]},{"type":"bullet_list","content":[{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"execution-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/execution-guide.md","title":null}}]},{"text":" — Fallback paths (curl standard/instruct/streaming, code generation, autonomous)","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"api-guide.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/api-guide.md","title":null}}]},{"text":" — API supplementary guide","type":"text"}]}]},{"type":"list_item","content":[{"type":"paragraph","content":[{"text":"sources.md","type":"text","marks":[{"type":"link","attrs":{"href":"references/sources.md","title":null}}]},{"text":" — Official documentation URLs","type":"text"}]}]}]}]},"metadata":{"date":"2026-11-02","name":"qwencloud-audio-tts","author":"@skillopedia","source":{"stars":44,"repo_name":"qwencloud-ai","origin_url":"https://github.com/QwenCloud/qwencloud-ai/blob/HEAD/skills/audio/qwencloud-audio-tts/SKILL.md","repo_owner":"QwenCloud","body_sha256":"0ec61e8e56c4e967c44995518a71d6bb4949081ed77598b2584d17a70c8a88b1","cluster_key":"c18d5fdfaa8ac2c30b6422a2b62112505116d7af17798edf45a067854347467d","clean_bundle":{"format":"clean-skill-bundle-v1","source":"QwenCloud/qwencloud-ai/skills/audio/qwencloud-audio-tts/SKILL.md","text_bytes":135355,"source_commit":"60542ab371ae2adc04937a3fdbbafe7baad7738c","skills_sh_rank":9431,"attachment_count":14,"text_attachments":14,"attachment_storage":"skill_attachments","binary_attachments":0},"cluster_size":1,"skill_md_path":"skills/audio/qwencloud-audio-tts/SKILL.md","import_metadata":{"date":"2026-11-02","author":"@skillopedia","version":"v1","category":"media-content","category_label":"Media"},"exact_dupes_collapsed_into_this":0},"version":"v1","category":"media-content","import_tag":"skills-sh-top20k-2026-11","description":"Synthesize speech from text with Qwen TTS models. TRIGGER when: user wants to convert text to speech, create voiceovers, generate audio narration, read text aloud, build TTS applications, mentions speech synthesis/voice generation/audio output from text, or explicitly invokes this skill by name (e.g. use qwencloud-audio-tts). DO NOT TRIGGER when: user wants speech recognition/ASR, text generation without audio, non-Qwen audio tasks.","compatibility":"Requires Python 3.9+; curl is PAYG-only. Cursor: auto-loaded. Claude Code: read this skill's SKILL.md before first use."}},"renderedAt":1793666551731}

Qwen Audio TTS (Text-to-Speech) Synthesize natural speech from text using Qwen TTS models. This skill is part of qwencloud/qwencloud-ai . Skill directory Use this skill's internal files to execute and learn. Load reference files on demand when the default path fails or you need details. | Location | Purpose | |----------|---------| | | Qwen TTS / Qwen Audio TTS (HTTP and WebSocket); current compatibility and defaults are in the model catalog below | | | CosyVoice (WebSocket API) — requires SDK; current compatibility and defaults are in the model catalog below | | | CosyVoice setup, voices, ex…