Skill: zuoyuting214/zyt-tts-voice-clone (version 0.4.0)

--- name: chanjing-tts-voice-clone description: Use Chanjing TTS API to synthesize speech from text, using user-provided voice metadata: openclaw: homepage: https://open-api.chanjing.cc --- # Chanjing TTS Voice Clone ## When to Use This Skill Use this skill when the user needs to generate speech from text, with a user-provided reference voice. The reference audio needs to be provided as a publicly accessible url. This TTS service supports: * bilingual Chinese and English * adjustment of speech rate * sentence-level timestamp ## How to Use This Skill **前置条件（本地配置与鉴权）**：本 Skill 自己包含本地配置和鉴权流程，不依赖其他 skill 的运行时脚本。 默认读取 `~/.chanjing/credentials.json`；若设置 `CHANJING_CONFIG_DIR`，则读取 `$CHANJING_CONFIG_DIR/credentials.json`。 API 固定使用 `https://open-api.chanjing.cc`。 当本地缺少 AK/SK 或 AK/SK 无效时，脚本默认返回登录引导信息，不自动打开浏览器。 如需本地自动开页，可显式设置：`CHANJING_AUTO_OPEN_LOGIN=1`。登录页：`https://www.chanjing.cc/openapi/login`。 Chanjing-TTS-Voice-Clone provides an asynchronous speech synthesis API. Hostname for all APIs is: "https://open-api.chanjing.cc". All requests communicate using json. You should use utf-8 to encode and decode text throughout this task. 1. Obtain an access\_token, which is required for subsequent requests 2. Call the Create Voice API, which accepts a url to an audio file as reference voice 3. Poll the Query Voice API until the result is success; keep the voice ID 4. Call the Create Speech Generation Task API, using the voice ID, record the `task_id` 5. Poll the Query Speech Generation Task Status API until the result is success 6. When the task status is complete, use the corresponding url in API response to download the generated audio file ### Get Access Token API 从 `~/.chanjing/credentials.json` 读取 `app_id` 和 `secret_key`，若无有效 Token 则调用： ```http POST /open/v1/access_token Content-Type: application/json ``` 请求体（使用本地配置的 app_id、secret_key）： ```json { "app_id": "<从 credentials.json 读取>", "secret_key": "<从 credentials.json 读取>" } ``` Response example: ```json { "trace_id": [REDACTED_SECRET], "code": 0, "msg": "success", "data": { "access_token": [REDACTED_SECRET], "expire_in": 1721289220 } } ``` Response field description: | First-level Field | Second-level Field | Description | |---|---|---| | code | | Response status code | | msg | | Response message | | data | | Response data | | | access\_token | Valid for one day, previous token will be invalidated | | | expire\_in | Token expiration time | Response status code description: | code | Description | |---|---| | 0 | Success | | 400 | Parameter format error | | 40000 | Parameter error | | 50000 | System internal error | ### Create Voice API Post to the following endpoint to create a voice. ```http POST /open/v1/create_customised_audio access_token: {{access_token}} Content-Type: application/json ``` Request body example: ```json { "name": "example", "url": "https://example.com/abc.mp3" } ``` Request field description: | Field | Type | Required | Description | |---|---|---|---| | name | string | Yes | A name for this voice | | url | string | Yes | url to the reference audio file, format must be one of mp3, wav or m4a. Supported mime: audio/x-wav, audio/mpeg, audio/m4a, video/mp4. Size must not exceed 100MB. Recommended audio length: 30s-5min | | model\_type | string | Yes | Use "Cicada3.0-turbo" | | language | string | No | Either "cn" or "en", default to "cn" | Response example: ```json { "trace_id": [REDACTED_SECRET], "code": 0, "msg": "success", "data": [REDACTED_SECRET] } ``` Response field description: | Field | Description | |---|---| | code | Status Code | | msg | Message | | data | Voice ID, to be used in following steps | Response status code description: | Code | Description | |---|---| | 0 | Success | | 400 | Parameter Format Error | | 10400 | AccessToken Error | | 40000 | Parameter Error | | 40001 | QPS Exceeded | | 50000 | Internal System Error | ### Poll Voice API Send a GET request to the following endpoint to query whether the voice is ready to be used, voice ID is obtained in the previous step. The polling process may take a few minutes, keep polling until the status indicates the voice is ready. ```http GET /open/v1/customised_audio?id={{voice_id}} access_token: {{access_token}} ``` Response example: ```json { "trace_id": [REDACTED_SECRET], "code": 0, "msg": "success", "data": { "id": [REDACTED_SECRET], "name": "声音克隆", "type": "cicada1.0", "progress": 0, "audio_path": "", "err_msg": "不支持的音频格式，请阅读接口文档", "status": 2 } } ``` Response field description: | First-level Field | Second-level Field | Description | |---|---|---| | code | | Status Code | | msg | | Response Message | | data | | | | | id | Voice ID | | | progress | Progress: range 0-100 | | | type | | | | name | | | | err\_msg | Error Message | | | audio\_path | | | | status | 0-queued; 1-in progress; 2-done; 3-expired; 4-failed; 99-deleted | Response status code description: | Code | Description | |---|---| | 0 | Success | | 10400 | AccessToken Error | | 40000 | Parameter Error | | 40001 | QPS Exceeded | | 50000 | Internal System Error | ### Create Speech Generation Task API Post to the following endpoint to submit a speech generation task: ```http POST /open/v1/create_audio_task access_token: {{access_token}} Content-Type: application/json ``` Request body example: ```json { "audio_man": [REDACTED_SECRET], "speed": 1, "pitch": 1, "text": { "text": "Hello, I am your AI assistant." } } ``` Request field description: | Parameter Name | Type | Nested Key | Required | Example | Description | |---|---|---|---|---|---| | audio\_man | string | | Yes | C-Audio-53e4e53ba1bc40de91ffaa74f20470fc | Voice ID, obtained from previous step | | speed | number | | Yes | 1 | Speech rate, range: 0.5 (slow) to 2 (fast) | | pitch | number | | Yes | 1 | Pitch (always set to 1) | | text | object | text | Yes | Hello, I am your Cicada digital human | Rich text, text length limit less than 4,000 characters | | aigc\_watermark | bool | | No | false | Whether to add visible watermark to audio, default is false | Response field description: | Field | Description | |---|---| | code | Response status code | | msg | Response message | | task\_id | Speech synthesis task ID | Example Response ```json { "trace_id": [REDACTED_SECRET], "code": 0, "msg": "success", "data": { "task_id": [REDACTED_SECRET] } } ``` Response status code description: | code | Description | |---|---| | 0 | Success | | 400 | Incoming parameter format error | | 10400 | AccessToken verification failed | | 40000 | Parameter error | | 40001 | Exceeds QPS limit | | 40002 | Production duration reached limit | | 50000 | System internal error | #### Query Speech Generation Task Status API Post a request to the following endpoint: ```http POST /open/v1/audio_task_state access_token: {{access_token}} Content-Type: application/json ``` Request body example: ```json { "task_id": [REDACTED_SECRET] } ``` Request field description: | Parameter Name | Type | Required | Example | Description | |---|---|---|---|---| | task\_id | string | Yes | 88f789dd9b8e4a121abb9d4679e0edc8 | Task ID obtained in previous step | Response body example: ```json { "trace_id": [REDACTED_SECRET], "code": 0, "msg": "success", "data": { "id": [REDACTED_SECRET], "type": "tts", "status": 9, "text": [ "猫在跌落时能够在空中调整身体，通常能够四脚着地，这种”猫右自己“反射显示了它们惊人的身体协调能力和灵活性。核磁共振成像技术通过利用人体细胞中氢原子的磁性来生成详细的内部图像，为医学诊断提供了重要工具。" ], "full": { "url": "https://cy-cds-test-innovation.cds8.cn/chanjing/res/upload/tts/2025-04-08/093a59021d85a72d28a491f21820ece4.wav", "path": "093a59013d85a72d28a491f21820ece4.wav", "duration": 18.81 }, "slice": null, "errMsg": "", "errReason": "", "subtitles": [ { "key": [REDACTED_SECRET], "start_time": 0, "end_time": 2.77, "subtitle": "猫在跌落时能够在空中调整身体" }, { "key": [REDACTED_SECRET], "start_time": 2.77, "end_time": 4.49, "subtitle": "通常能够四脚着地" }, { "key": [REDACTED_SECRET], "start_time": 4.49, "end_time": 5.73, "subtitle": "这种，猫右自己" }, { "key": [REDACTED_SECRET], "start_time": 5.73, "end_time": 7.97, "subtitle": "反射显示了它们惊人的身体" }, { "key": [REDACTED_SECRET], "start_time": 7.97, "end_time": 9.45, "subtitle": "协调能力和灵活性" }, { "key": [REDACTED_SECRET], "start_time": 9.45, "end_time": 12.49, "subtitle": "核磁共振成像技术通过利用人体" }, { "key": [REDACTED_SECRET], "start_time": 12.49, "end_time": 14.77, "subtitle": "细胞中氢原子的磁性来生成" }, { "key": [REDACTED_SECRET], "start_time": 14.77, "end_time": 15.97, "subtitle": "详细的内部图像" }, { "key": [REDACTED_SECRET], "start_time": 15.97, "end_time": 18.41, "subtitle": "为医学诊断提供了重要工具" } ] } } ``` Response field description: | First-level Field | Second-level Field | Third-level Field | Description | |---|---|---|---| | code | | | Response status code | | msg | | | Response message | | data | id | | Audio ID | | | type | | Speech type | | | status | | Status: 1 - in progress, 9 - done | | | text | | Speech text | | | full | url | url to download generated audio file | | | | path | | | | | duration | Audio duration | | | slice | | | | | errMsg | | Error message | | | errReason | | Error reason | | | subtitles(array type) | key | Subtitle ID | | | | start\_time | Subtitle start time point | | | | end\_time | Subtitle end time point | | | | subtitle | Subtitle text | Response field description: | Code | Description | |---|---| | 0 | Response successful | | 10400 | AccessToken verification failed | | 40000 | Parameter error | | 50000 | System internal error | ## Scripts 本 Skill 提供脚本（`scripts/`）： | 脚本 | 说明 | |------|------| | `chanjing-config` | 写入/查看本地 `app_id` 与 `secret_key`，并清理旧 token 缓存 | | `chanjing-get-token` | 从本地凭证获取有效 `access_token`（必要时自动刷新） | | `create_voice` | 提交定制声音任务（参考音频 URL），输出 voice_id | | `poll_voice` | 轮询定制声音直到就绪（status=2），输出 voice_id | | `create_task` | 使用定制声音创建 TTS 任务，输出 task_id | | `poll_task` | 轮询 TTS 任务直到完成，输出音频下载 URL | 示例（在项目根或 skill 目录下执行）： ```bash # 1. 创建定制声音（参考音频需为公开 URL） VOICE_ID=$(python scripts/create_voice --name "我的声音" --url "https://example.com/ref.mp3") # 2. 轮询直到声音就绪 python scripts/poll_voice --voice-id "$VOICE_ID" # 3. 创建 TTS 任务 TASK_ID=$(python scripts/create_task --audio-man "$VOICE_ID" --text "Hello, I am your AI assistant.") # 4. 轮询到完成，得到音频下载链接 python scripts/poll_task --task-id "$TASK_ID" ```

path: scripts/_auth.py
content: # 鉴权：从 CONFIG_DIR/credentials.json 读取 AK/SK
# 实现：AK/SK 校验、Token 校验与刷新；默认不自动打开浏览器，仅返回登录引导信息
import json
import os
import time
import urllib.request
from pathlib import Path

CONFIG_DIR = Path(os.environ.get("CHANJING_CONFIG_DIR", Path.home() / ".chanjing"))
CONFIG_FILE = CONFIG_DIR / "credentials.json"
API_BASE = "https://open-api.chanjing.cc"
BUFFER_SECONDS = 300
LOGIN_URL = "https://www.chanjing.cc/openapi/login"
AUTO_OPEN_LOGIN = os.environ.get("CHANJING_AUTO_OPEN_LOGIN", "").strip().lower() in {"1", "true", "yes", "on"}

NO_CREDENTIALS_MSG = """缺少 AK/SK 本地凭证。
请先在 ~/.chanjing/credentials.json（或 $CHANJING_CONFIG_DIR/credentials.json）中配置：
  {
    "app_id": "<你的app_id>",
    "secret_key": "<你的secret_key>"
  }
登录页：https://www.chanjing.cc/openapi/login
配置完成后请重新执行您之前的操作。"""

INVALID_CREDENTIALS_MSG = """AK/SK 无效或已过期。
请重新获取 app_id 和 secret_key，并更新 ~/.chanjing/credentials.json
（或 $CHANJING_CONFIG_DIR/credentials.json）后重试。
登录页：https://www.chanjing.cc/openapi/login"""


def _run_open_login_page():
    """按配置决定是否尝试打开登录页。"""
    if not AUTO_OPEN_LOGIN:
        return
    try:
        import webbrowser

        webbrowser.open(LOGIN_URL)
    except Exception:
        pass


def read_config():
    if CONFIG_FILE.exists():
        with open(CONFIG_FILE, "r", encoding="utf-8") as f:
            return json.load(f)
    return {}


def write_config(data):
    CONFIG_DIR.mkdir(parents=True, exist_ok=True)
    try:
        os.chmod(CONFIG_DIR, 0o700)
    except Exception:
        pass
    with open(CONFIG_FILE, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, ensure_ascii=False)
    try:
        os.chmod(CONFIG_FILE, 0o600)
    except Exception:
        pass


def clear_cached_token():
    """清除本地缓存的 access_token / expire_in。"""
    data = read_config()
    data.pop("access_token", None)
    data.pop("expire_in", None)
    write_config(data)


def _request_new_token(app_id, secret_key):
    url = API_BASE + "/open/v1/access_token"
    req = urllib.request.Request(
        url,
        data=json.dumps({"app_id": app_id, "secret_key": secret_key}).encode("utf-8"),
        headers={"Content-Type": "application/json"},
        method="POST",
    )
    with urllib.request.urlopen(req, timeout=30) as resp:
        return json.loads(resp.read().decode("utf-8"))


def get_token(force_refresh=False):
    """返回 (token, None) 或 (None, error_msg)。"""
    data = read_config()
    app_id = (data.get("app_id") or "").strip()
    secret_key = (data.get("secret_key") or "").strip()
    if not app_id or not secret_key:
        _run_open_login_page()
        return None, NO_CREDENTIALS_MSG

    now = int(time.time())
    [REDACTED_SECRET]"access_token") or "").strip()
    expire_in = data.get("expire_in")
    try:
        expire_in = int(expire_in) if expire_in is not None else 0
    except (ValueError, TypeError):
        expire_in = 0

    if (not force_refresh) and token and expire_in > now + BUFFER_SECONDS:
        return token, None

    try:
        body = _request_new_token(app_id, secret_key)
    except Exception as e:
        return None, str(e)

    if body.get("code") != 0:
        _run_open_login_page()
        return None, INVALID_CREDENTIALS_MSG

    d = body.get("data", {})
    new_token = d.get("access_token")
    if not new_token:
        _run_open_login_page()
        return None, INVALID_CREDENTIALS_MSG

    data["access_token"] = new_token
    data["expire_in"] = d.get("expire_in")
    write_config(data)
    return new_token, None


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