Skill: jsinbupt/senseaudio-voice-broadcast (version 1.0.1)

--- name: character-voice-broadcast description: Turn plain text into character-style TTS scripts for narration, companion-style voice messages, emotional comfort audio, and expressive spoken responses. Use when Codex needs to prepare or call TTS for human-like voice broadcast, role-based narration, emotional companionship, bedtime or greeting voice messages, soothing or encouraging speech, or any task where speech should sound like a specific persona rather than neutral text-to-speech. --- # Character Voice Broadcast ## Overview Use this skill to convert plain text into TTS-ready, persona-driven spoken content. Prioritize natural spoken rhythm, stable persona traits, and emotional control over literal text fidelity. ## Core Use Cases Apply this skill for requests like: - "把这段文案做成更像真人的语音播报" - "给这个角色做一段晚安陪伴语音" - "把这段台词改成更适合 TTS 朗读的人物化版本" - "做一个温柔安慰型的语音回复" - "让这段播报听起来像角色在说，不要像机器念稿" ## Workflow Follow this order unless the user already specifies some of it. ### 1. Identify the speaking role Decide who is speaking: - narrator - fictional character - companion-style persona - news or info host - comfort / encouragement voice - greeting / wake-up / bedtime voice If the user names a persona, keep that identity stable through the whole output. If the user does not provide a persona, infer one from the task and state it clearly in the output. ### 2. Choose the target speaking effect Select one primary effect: - `播报型`: clear, structured, easy to follow - `陪伴型`: warm, soft, intimate, less formal - `角色型`: strong personality markers and stylized wording - `安慰型`: gentle, slower, emotionally safe - `激励型`: energetic, uplifting, rhythmic Do not mix conflicting effects unless the user explicitly asks for it. ### 3. Rewrite the text for speech, not for reading Transform the source text into spoken language: - shorten overly long sentences - remove written-only phrasing - add natural pauses - add light fillers only when they improve realism - make emotional turns audible, not just semantic - keep key facts intact if the text contains information the user needs TTS scripts should sound like someone speaking to a listener, not like a document being read aloud. ### 4. Produce two layers of output When helpful, output both: - `口播文本`: the final text to send to TTS - `语音控制说明`: style notes for the TTS backend, such as tone, pace, pause density, warmth, smile, tenderness, or firmness If the user asks for direct TTS calling instructions, also output backend-facing hints in short bullet form. ### 4.1 Call SenseAudio TTS when audio output is needed If the task is not just script writing but actual audio generation, call the bundled script: ```bash python3 scripts/senseaudio_tts.py \ --text "你好，欢迎体验 SenseAudio 带来的极致语音服务。" \ --voice-id male_0004_a \ --output output.wav ``` Authentication: - read API key from `SENSEAUDIO_API_KEY` - or pass `--api-key` Default endpoint and model: - endpoint: `https://api.senseaudio.cn/v1/t2a_v2` - model: `SenseAudio-TTS-1.0` Use `--text-file` when the spoken script is long or already saved as a file. Prefer generating the spoken script first, then sending that cleaned script to TTS instead of sending raw written prose. ### 5. Match emotion to context Use emotion deliberately: - comfort: lower intensity, slower pacing, gentle reassurance - companion chat: natural rhythm, light intimacy, occasional casual phrasing - dramatic character: more contrast, more stylized keywords, sharper scene cues - narration: stable clarity, less filler, clean structure - bedtime: soft cadence, low stimulation, short phrases Avoid exaggerated emotional markers that would make TTS sound unnatural or theatrical unless explicitly requested. ## Output Rules Default to Chinese output unless the user requests another language. Prefer short spoken units over long written paragraphs. Preserve concrete facts, names, numbers, and instructions unless the user asks for a looser adaptation. If the task is informational broadcast, keep it understandable first and expressive second. If the task is emotional companionship, keep it emotionally coherent first and factually safe second. When preparing a TTS-friendly script: - replace stiff书面语 with口语 - break dense exposition into 1-3 sentence chunks - mark meaningful pauses with punctuation, not stage directions - avoid overusing ellipses or repeated interjections ## Standard Deliverables Produce the smallest useful set: - final spoken script for TTS - persona summary - TTS style control notes - optional alternate versions if the user asks for multiple moods - optional SenseAudio invocation command or generated audio file path ## Default Chinese Templates Use the templates in [voice-broadcast-templates.md](references/voice-broadcast-templates.md) when the user does not specify a format. ## Resources - [voice-broadcast-templates.md](references/voice-broadcast-templates.md): Chinese output templates - [senseaudio-api.md](references/senseaudio-api.md): SenseAudio API usage notes - [senseaudio_tts.py](scripts/senseaudio_tts.py): local wrapper for generating audio with SenseAudio ## Trigger Examples Use this skill for requests like: - "把这段介绍文案改成像真人角色播报" - "做一段安慰人的陪伴语音文案" - "把这段台词整理成适合 TTS 的角色对白" - "给我一版温柔女生口播和一版冷静男声口播" - "为这个角色准备晨间问候和晚安语音"

path: agents/openai.yaml
content: interface:
  display_name: "Character Voice Broadcast"
  short_description: "Character-style TTS for narration and comfort."
  default_prompt: "Use $character-voice-broadcast to rewrite this text into a persona-driven TTS script with voice style notes."


path: references/senseaudio-api.md
content: # SenseAudio TTS 接入说明

## 接口

- URL: `https://api.senseaudio.cn/v1/t2a_v2`
- Method: `POST`
- Header:
  - `Authorization: Bearer <API_KEY>`
  - `Content-Type: application/json`

## 最小请求体

```json
{
  "model": "SenseAudio-TTS-1.0",
  "text": "你好，欢迎体验 SenseAudio 带来的极致语音服务。",
  "voice_setting": {
    "voice_id": "male_0004_a"
  }
}
```

## Skill 内推荐流程

1. 先把原始文本改写成适合 TTS 的口播文本
2. 再确定 `voice_id`
3. 再调用 `scripts/senseaudio_tts.py`
4. 保存输出音频到本地路径

## 推荐环境变量

- `SENSEAUDIO_API_KEY`: SenseAudio API key

## 推荐调用示例

```bash
export SENSEAUDIO_API_KEY="your_api_key"

python3 scripts/senseaudio_tts.py \
  --text "你好，欢迎来到今晚的陪伴时刻。" \
  --voice-id male_0004_a \
  --output ./senseaudio_output.wav
```

## 使用建议

- 先做人物化改写，再送入 TTS
- 文本太长时优先分段
- 陪伴型内容适合偏慢语速和更柔和的措辞
- 播报型内容优先保证信息清晰


path: references/voice-broadcast-templates.md
content: # 人物化 TTS 中文模板

在角色播报、陪伴语音、情绪化口播场景下，优先使用下面的模板。

## 1. 标准输出模板

```md
# 人物化语音方案

## 1. 角色设定
- 角色名：
- 角色定位：
- 年龄感：
- 性格关键词：
- 与听众关系：

## 2. 语音目标
- 使用场景：
- 目标情绪：
- 输出时长：
- 语气要求：

## 3. TTS口播文本
- 

## 4. TTS风格控制说明
- 语速：
- 停顿：
- 情绪强度：
- 亲密度：
- 清晰度：
- 是否带轻微口语词：
```

## 2. 情感陪伴模板

```md
# 陪伴语音方案

## 1. 角色关系
- 说话人是谁：
- 想传递的陪伴感：

## 2. 情绪策略
- 先安抚什么情绪：
- 再给什么支持：
- 最后如何收尾：

## 3. TTS口播文本
- 

## 4. 语音参数建议
- 建议语速：偏慢
- 建议停顿：中等偏多
- 建议语气：温柔、稳定、不过度煽情
```

## 3. 角色播报模板

```md
# 角色播报方案

## 1. 角色识别点
- 角色名：
- 性格：
- 典型说话方式：
- 禁止出现的风格偏差：

## 2. 播报目标
- 播报内容类型：
- 听众是谁：
- 要突出什么信息：

## 3. TTS口播文本
- 

## 4. 语音控制说明
- 语速：
- 节奏：
- 感情起伏：
- 重点词：
```

## 4. 晨间 / 晚安模板

```md
# 问候语音方案

## 1. 使用时段
- 晨间 / 晚安

## 2. 目标感受
- 希望听众感受到什么：

## 3. TTS口播文本
- 

## 4. 风格建议
- 语速：
- 音色倾向：
- 情绪强度：
```

## 5. 快速风格标签

当用户没有写太多细节时，可优先从这些标签里选：
- 温柔陪伴
- 清冷角色
- 元气播报
- 治愈安慰
- 成熟可靠
- 轻松闲聊
- 深夜电台

## 6. 防机器感检查清单

生成前检查：
- 是否仍像书面稿而不是说话
- 是否单句太长
- 是否过度堆叠感叹词
- 是否情绪过猛导致像配音腔
- 是否缺少停顿和轻重变化
- 是否忘记保留用户要求的事实信息


path: scripts/senseaudio_tts.py
content: #!/usr/bin/env python3
import argparse
import base64
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request


DEFAULT_URL = "https://api.senseaudio.cn/v1/t2a_v2"
DEFAULT_MODEL = "SenseAudio-TTS-1.0"


def parse_args() -> argparse.Namespace:
    parser = argparse.ArgumentParser(
        description="Call SenseAudio TTS and save the result to a local file."
    )
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--text", help="Text to synthesize.")
    group.add_argument("--text-file", help="Path to a UTF-8 text file to synthesize.")
    parser.add_argument("--voice-id", required=True, help="SenseAudio voice_id.")
    parser.add_argument("--output", required=True, help="Output audio file path.")
    parser.add_argument("--api-key", help="SenseAudio API key. Defaults to SENSEAUDIO_API_KEY.")
    parser.add_argument("--model", default=DEFAULT_MODEL, help="Model name.")
    parser.add_argument("--url", default=DEFAULT_URL, help="SenseAudio API URL.")
    return parser.parse_args()


def load_text(args: argparse.Namespace) -> str:
    if args.text is not None:
        return args.text
    with open(args.text_file, "r", encoding="utf-8") as f:
        return f.read().strip()


def build_request(args: argparse.Namespace, text: str) -> urllib.request.Request:
    [REDACTED_SECRET] or os.environ.get("SENSEAUDIO_API_KEY")
    if not api_key:
        raise SystemExit("Missing API key. Set SENSEAUDIO_API_KEY or pass --api-key.")

    payload = {
        "model": args.model,
        "text": text,
        "voice_setting": {"voice_id": args.voice_id},
    }
    data = json.dumps(payload).encode("utf-8")
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json",
    }
    return urllib.request.Request(args.url, data=data, headers=headers, method="POST")


def ensure_parent(path: str) -> None:
    parent = os.path.dirname(os.path.abspath(path))
    if parent:
        os.makedirs(parent, exist_ok=True)


def save_bytes(path: str, data: bytes) -> None:
    ensure_parent(path)
    with open(path, "wb") as f:
        f.write(data)


def maybe_download_url(url: str) -> bytes:
    with urllib.request.urlopen(url) as resp:
        return resp.read()


def extract_audio_bytes(body: bytes, content_type: str) -> bytes:
    if content_type.startswith("audio/") or content_type == "application/octet-stream":
        return body

    payload = json.loads(body.decode("utf-8"))

    encoded_candidates = [
        ("hex", payload.get("audio")),
        ("base64", payload.get("audio_base64")),
        ("hex", payload.get("data", {}).get("audio")),
        ("base64", payload.get("data", {}).get("audio_base64")),
        ("hex", payload.get("result", {}).get("audio")),
        ("base64", payload.get("result", {}).get("audio_base64")),
    ]
    for encoding, item in encoded_candidates:
        if isinstance(item, str) and item:
            if encoding == "hex":
                return bytes.fromhex(item)
            return base64.b64decode(item, validate=True)

    url_candidates = [
        payload.get("audio_url"),
        payload.get("url"),
        payload.get("data", {}).get("audio_url"),
        payload.get("data", {}).get("url"),
        payload.get("result", {}).get("audio_url"),
        payload.get("result", {}).get("url"),
    ]
    for item in url_candidates:
        if isinstance(item, str) and item:
            return maybe_download_url(item)

    raise ValueError(
        "Unable to find audio content in response. "
        "Response body was JSON but no known audio field was present."
    )


def main() -> int:
    args = parse_args()
    text = load_text(args)
    if not text:
        raise SystemExit("Input text is empty.")

    request = build_request(args, text)
    try:
        with urllib.request.urlopen(request) as resp:
            body = resp.read()
            content_type = resp.headers.get_content_type()
    except urllib.error.HTTPError as exc:
        detail = exc.read().decode("utf-8", errors="ignore")
        print(f"HTTP {exc.code}: {detail}", file=sys.stderr)
        return 1
    except urllib.error.URLError as exc:
        print(f"Request failed: {exc}", file=sys.stderr)
        return 1

    try:
        audio_bytes = extract_audio_bytes(body, content_type)
    except Exception as exc:
        print(f"Failed to parse response: {exc}", file=sys.stderr)
        return 1

    save_bytes(args.output, audio_bytes)
    print(os.path.abspath(args.output))
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


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