Skill: lanlan314/qwen-tts-lan (version 1.0.2)

--- name: qwen-tts description: 阿里云千问语音合成（TTS）技能，支持将文本转换为自然语音。当用户要求朗读、语音合成、文字转语音、TTS、读一段话、把文字转成声音时使用。支持多种音色（中文/英文/方言），支持流式输出边合成边播放。 --- # 千问语音合成技能 (qwen-tts) ## ⚠️ 环境变量配置 ### 基础配置（使用任何脚本都需要） | 环境变量 | 说明 | 获取方式 | |----------|------|---------| | `DASHSCOPE_API_KEY` | 千问 API 密钥 | [阿里云百炼控制台](https://dashscope.console.aliyun.com) | ### 飞书配置（仅 speak_and_send.py 需要） `speak.sh` 只需要 DASHSCOPE_API_KEY。如果需要发送语音到飞书，还需配置： | 环境变量 | 说明 | 获取方式 | |----------|------|---------| | `FEISHU_APP_ID` | 飞书应用 App ID | 飞书开放平台应用凭证 | | `FEISHU_APP_SECRET` | 飞书应用 App Secret | 飞书开放平台应用凭证 | | `FEISHU_USER_ID` | 接收语音的飞书用户 ID | 飞书用户 open_id | **最小配置（只需 DASHSCOPE_API_KEY）：** ```bash export DASHSCOPE_API_KEY="sk-xxxxx" ``` **完整配置（包含飞书发送）：** ```bash export DASHSCOPE_API_KEY="sk-xxxxx" export FEISHU_APP_ID="cli_xxxxx" export FEISHU_APP_SECRET="xxxxx" export FEISHU_USER_ID="ou_xxxxx" ``` ## 快速使用 ### 基本语音合成（同步接口） 使用 curl 调用千问 TTS： ```bash curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "qwen3-tts-flash", "input": { "text": "要转换的文本内容", "voice": "Cherry", "language_type": "Chinese" } }' ``` ### 常用模型 | 模型 | 特点 | 适用场景 | |------|------|---------| | `qwen3-tts-flash` | 快速、便宜 | 短文本、导航、通知、教育课件 | | `qwen3-tts-instruct-flash` | 支持指令控制情感 | 有声书、广播剧、游戏配音 | | `qwen3-tts-vd` | 声音设计 | 品牌定制、从零设计音色 | | `qwen3-tts-vc` | 声音复刻 | 基于样本复刻真人音色 | ### 常用音色（voice 参数） | 音色名 | 语言/风格 | 说明 | |--------|-----------|------| | `Cherry` | 中文 | 女声，活泼 | | `Azure` | 英文 | 女声，标准 | | `Alexander` | 英文 | 男声 | | `Huogeng` | 中文 | 女声，温柔 | | `Shanbin` | 中文 | 男声，沉稳 | | `Emma` | 英文 | 女声，轻快 | > 更多音色请参见 [references/voices.md](references/voices.md) ### 常用参数 | 参数 | 说明 | 默认值 | |------|------|-------| | `text` | 要转换的文本，建议不超过300字符 | 必填 | | `voice` | 音色名称 | `Cherry` | | `language_type` | 文本语言：`Chinese` / `English` / `yue`（粤语）等 | 自动检测 | | `instructions` | 情感/风格指令（仅 instruct 模型） | - | ### 输出格式 音频 URL 有效期 **24小时**，返回格式为 wav。 ## 执行流程 1. **检查环境变量**：确保 `DASHSCOPE_API_KEY` 已设置 2. **构建请求**：根据文本和音色参数构建 JSON 3. **调用 API**：POST 到千问 TTS 接口 4. **下载音频**：从响应中提取 URL 并下载 5. **返回结果**：音频文件路径或发送给你 ## 示例：中文朗读 ```bash curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "qwen3-tts-flash", "input": { "text": "你好，我是千问语音助手，今天天气真不错。", "voice": "Cherry", "language_type": "Chinese" } }' ``` ## 脚本说明 本技能包含两个脚本，按需使用： ### scripts/speak.sh 纯 Bash 脚本，**仅生成本地音频文件**，不需要飞书凭证 ```bash # 只需要 DASHSCOPE_API_KEY ./speak.sh "要转换的文本" [音色] # 输出: /tmp/qwen_tts_xxx.ogg ``` ### scripts/speak_and_send.py Python 脚本，**生成 TTS 并发送到飞书**，需要配置飞书凭证 ```bash # 需要 DASHSCOPE_API_KEY + FEISHU_* 环境变量 python3 speak_and_send.py "要说的文本" [音色] # 自动发送到配置的 FEISHU_USER_ID ``` ## 参考资料 完整音色列表和 API 文档请参见： - [references/voices.md](references/voices.md) - 全部音色列表 - [references/api.md](references/api.md) - 详细 API 说明 ## 依赖说明 - `ffmpeg` - 音频格式转换（脚本需要） - `jq` - JSON 处理（speak.sh 需要） - `python3` + `requests` - speak_and_send.py 需要

path: references/api.md
content: # 千问 TTS API 详细参考

## 基本信息

- **北京地域**: `https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
- **新加坡地域**: `https://dashscope-intl.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
- **音频URL有效期**: 24小时

## 环境变量

```bash
export DASHSCOPE_API_KEY="your-api-key"
```

## 同步接口（非流式）

### 请求示例

```bash
curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3-tts-flash",
    "input": {
      "text": "那我来给大家推荐一款T恤，这款呢真的是超级好看。",
      "voice": "Cherry",
      "language_type": "Chinese"
    }
  }'
```

### 请求参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| model | string | 是 | 模型名：`qwen3-tts-flash`、`qwen3-tts-instruct-flash`、`qwen3-tts-vd`、`qwen3-tts-vc` |
| input.text | string | 是 | 要转换的文本，建议不超过300字符 |
| input.voice | string | 是 | 音色名称 |
| input.language_type | string | 是 | 语言：`Chinese` / `English` / `yue` 等 |
| input.instructions | string | 否 | 情感指令（仅 instruct 模型） |
| parameters.optimize_instructions | bool | 否 | 优化指令（仅 instruct 模型） |

### 响应参数

```json
{
  "output": {
    "choices": [{
      "finish_reason": "stop",
      "message": {
        "content": [{
          "audio_url": "https://dashscope-result-xxx.wav"
        }]
      }
    }]
  },
  "usage": {
    "audio_count": 1
  },
  "request_id": "xxx"
}
```

## Python SDK 示例

```python
import os
import dashscope

dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'

response = dashscope.MultiModalConversation.call(
    model="qwen3-tts-flash",
    [REDACTED_SECRET]"DASHSCOPE_API_KEY"),
    text="你好，欢迎使用千问语音合成",
    voice="Cherry",
    language_type="Chinese",
    stream=False
)

if response.status_code == 200:
    audio_url = response.output['choices'][0]['message']['content'][0]['audio_url']
    print(f"音频URL: {audio_url}")
else:
    print(f"错误: {response.code} - {response.message}")
```

## 流式输出

```python
import pyaudio
import base64

response = dashscope.MultiModalConversation.call(
    model="qwen3-tts-flash",
    [REDACTED_SECRET]"DASHSCOPE_API_KEY"),
    text="你好",
    voice="Cherry",
    language_type="Chinese",
    stream=True  # 开启流式
)

p = pyaudio.PyAudio()
stream = p.open(format=pyaudio.paInt16, channels=1, rate=24000, output=True)

for chunk in response:
    if chunk.output.choices[0].message.content:
        audio_data = base64.b64decode(chunk.output.choices[0].message.content[0].audio_data)
        stream.write(audio_data)
```

## 常见错误码

| 错误码 | 说明 | 解决方案 |
|--------|------|---------|
| InvalidApiKey | API Key 无效 | 检查 DASHSCOPE_API_KEY |
| InvalidParameter | 参数错误 | 检查 text、voice、language_type |
| RateLimitExceed | 限流 | 降低请求频率 |
| TextTooLong | 文本过长 | 拆分文本为多个请求 |
| UnsupportedModel | 不支持的模型 | 使用支持的模型 |


path: references/voices.md
content: # 千问 TTS 音色列表

## 系统音色（无需定制，直接使用）

### 中文音色

| 音色名 | 性别 | 风格 | 适用场景 |
|--------|------|------|---------|
| `Cherry` | 女 | 活泼甜美 | 客服、互动、教学 |
| `Huogeng` | 女 | 温柔知性 | 有声书、情感内容 |
| `Shanbin` | 男 | 沉稳磁性 | 新闻、导航、讲解 |
| `Eason` | 男 | 年轻活力 | 品牌广告、短视频 |
| `Yunyang` | 男 | 成熟专业 | 知识讲解、纪录片 |
| `Junhou` | 男 | 亲切温和 | 儿童内容、故事 |
| `Nini` | 女 | 清新自然 | 教育、互动 |
| `Yexun` | 男 | 大气厚重 | 纪录片、史诗 |
| `Zhuoma` | 女 | 藏族女声 | 民族特色内容 |
| `Mingyue` | 女 | 古典优雅 | 古风、有声书 |

### 英文音色

| 音色名 | 性别 | 风格 | 适用场景 |
|--------|------|------|---------|
| `Emma` | 女 | 轻快自然 | 品牌、广告、互动 |
| `Ashley` | 女 | 专业知性 | 企业介绍、教学 |
| `Laura` | 女 | 温暖友好 | 有声书、故事 |
| `Clara` | 女 | 清晰标准 | 客服、导航 |
| `Alice` | 女 | 活泼可爱 | 儿童内容 |
| `Jennifer` | 女 | 自然流畅 | 长文本朗读 |
| `Arnold` | 男 | 专业稳重 | 企业、新闻 |
| `Daniel` | 男 | 磁性低沉 | 纪录片、故事 |
| `Eric` | 男 | 年轻活力 | 短视频、社交 |
| `Ryan` | 男 | 阳光开朗 | 品牌、运动 |
| `William` | 男 | 英式古典 | 高端品牌、历史 |

### 粤语音色

| 音色名 | 性别 | 适用场景 |
|--------|------|---------|
| `Yixing` | 女 | 粤语对话、教育 |
| `Jiamu` | 男 | 粤语新闻、讲解 |

### 中文方言

| 音色名 | 方言 | 地区 |
|--------|------|------|
| `Zhiyu` | 四川话 | 四川 |
| `Yixuan` | 东北话 | 东北 |
| `Qiao` | 台湾话 | 台湾 |

## 声音定制音色

### 声音设计（Voice Design）- qwen3-tts-vd

通过文本描述创建全新音色，适合品牌定制：

```bash
curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3-tts-vd",
    "input": {
      "text": "欢迎来到我们的品牌世界",
      "voice": "young_female_excited_brand_ambassador"
    }
  }'
```

### 声音复刻（Voice Clone）- qwen3-tts-vc

基于音频样本复刻真人音色：

```bash
curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3-tts-vc",
    "input": {
      "text": "要复刻的文本内容"
    },
    "parameters": {
      "voice_id": "your-voice-id"
    }
  }'
```

## 指令控制（Instruct）

仅 `qwen3-tts-instruct-flash` 支持，用自然语言控制情感和风格：

```bash
curl -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
  -H "Authorization: Bearer $DASHSCOPE_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "qwen3-tts-instruct-flash",
    "input": {
      "text": "今天真的好开心啊！",
      "voice": "Cherry",
      "language_type": "Chinese",
      "instructions": "语速较快，带有明显的上扬语调，表达兴奋和喜悦"
    },
    "parameters": {
      "optimize_instructions": true
    }
  }'
```

常用指令示例：
- "语速较慢，低沉有力，适合新闻播报"
- "带有明显的上扬语调，表达兴奋"
- "温柔亲切，像朋友聊天"
- "庄重正式，适合企业介绍"


path: scripts/speak.sh
content: #!/bin/bash
# 千问 TTS 语音生成（仅生成本地音频文件，不发送飞书）
# 用法: ./speak.sh "要说话的文本" [音色名]
# 默认音色: Nofish
# 注意: 此脚本只需要 DASHSCOPE_API_KEY，不需要飞书凭证

TEXT="$1"
VOICE="${2:-Nofish}"  # 默认用 Nofish 音色
LANG="${3:-Chinese}"

if [ -z "$TEXT" ]; then
    echo "用法: $0 \"文本\" [音色] [语言]"
    exit 1
fi

[REDACTED_SECRET]"
if [ -z "$API_KEY" ]; then
    echo "错误: DASHSCOPE_API_KEY 未设置"
    exit 1
fi

TMP_WAV="/tmp/qwen_tts_$$.wav"
TMP_OGG="/tmp/qwen_tts_$$.ogg"

# 调用千问 API
RESPONSE=$(curl -s -X POST 'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation' \
    -H "Authorization: Bearer $API_KEY" \
    -H 'Content-Type: application/json' \
    -d "$(jq -n --arg text "$TEXT" --arg voice "$VOICE" --arg lang "$LANG" '{
        model: "qwen3-tts-flash",
        input: {
            text: $text,
            voice: $voice,
            language_type: $lang
        }
    }')")

# 解析音频 URL
AUDIO_URL=$(echo "$RESPONSE" | jq -r '.data.audio.url // .audio.url // empty')

if [ -z "$AUDIO_URL" ] || [ "$AUDIO_URL" == "null" ]; then
    echo "错误: 无法获取音频 URL"
    echo "响应: $RESPONSE"
    exit 1
fi

# 下载音频
curl -s -o "$TMP_WAV" "$AUDIO_URL"

# 转换为飞书支持的 ogg 格式
ffmpeg -i "$TMP_WAV" -c:a libopus -b:a 64k -ar 48000 "$TMP_OGG" -y 2>/dev/null

# 清理
rm -f "$TMP_WAV"

echo "$TMP_OGG"

path: scripts/speak_and_send.py
content: #!/usr/bin/env python3
"""
千问 TTS 语音生成并发送到飞书
- 默认音色: Nofish
- 末尾自动加1.5秒空白，防止语音被截断
用法: python3 speak_and_send.py "要说话的文本" [音色]
"""
import sys
import os
import json
import requests
import subprocess
import tempfile

# 飞书配置（从环境变量读取，用户必须自行配置）
FEISHU_APP_ID = os.environ.get('FEISHU_APP_ID', '')
FEISHU_APP_SECRET = os.environ.get('FEISHU_APP_SECRET', '')
FEISHU_USER_ID = os.environ.get('FEISHU_USER_ID', '')

if not FEISHU_APP_ID or not FEISHU_APP_SECRET or not FEISHU_USER_ID:
    raise ValueError("需要配置环境变量: FEISHU_APP_ID, FEISHU_APP_SECRET, FEISHU_USER_ID")


def get_feishu_token():
    resp = requests.post(
        'https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal',
        json={'app_id': FEISHU_APP_ID, 'app_secret': FEISHU_APP_SECRET}
    )
    return resp.json().get('tenant_access_token')


def generate_qwen_audio(text, voice="Nofish"):
    [REDACTED_SECRET]'DASHSCOPE_API_KEY', '')
    if not api_key:
        raise ValueError("DASHSCOPE_API_KEY not set")

    resp = requests.post(
        'https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation',
        headers={
            'Authorization': f'Bearer {api_key}',
            'Content-Type': 'application/json'
        },
        json={
            'model': 'qwen3-tts-flash',
            'input': {
                'text': text,
                'voice': voice,
                'language_type': 'Chinese'
            }
        }
    )
    data = resp.json()
    audio_url = (
        data.get('data', {}).get('audio', {}).get('url') or
        data.get('audio', {}).get('url') or
        data.get('output', {}).get('audio', {}).get('url')
    )
    if not audio_url:
        raise ValueError(f"Failed to get audio URL: {data}")
    return audio_url


def download_wav(url):
    resp = requests.get(url)
    fd, wav_path = tempfile.mkstemp(suffix='.wav')
    os.write(fd, resp.content)
    os.close(fd)
    return wav_path


def add_silence_and_convert(wav_path):
    """给音频末尾加1秒空白，转换为ogg"""
    silence_path = tempfile.mktemp(suffix='.wav')
    # 生成1秒静音
    subprocess.run([
        'ffmpeg', '-f', 'lavfi', '-i', 'anullsrc=r=48000:cl=mono', '-t', '1.5',
        silence_path, '-y'
    ], capture_output=True)

    # 拼接原音频 + 静音
    concat_list = tempfile.mktemp(suffix='.txt')
    with open(concat_list, 'w') as f:
        f.write(f"file '{wav_path}'\n")
        f.write(f"file '{silence_path}'\n")

    ogg_path = tempfile.mktemp(suffix='.ogg')
    subprocess.run([
        'ffmpeg', '-f', 'concat', '-safe', '0', '-i', concat_list,
        '-c:a', 'libopus', '-b:a', '64k', '-ar', '48000',
        ogg_path, '-y'
    ], capture_output=True)

    # 清理临时文件
    for f in [wav_path, silence_path, concat_list]:
        try:
            os.unlink(f)
        except:
            pass

    return ogg_path


def send_audio(token, ogg_path):
    with open(ogg_path, 'rb') as f:
        upload_resp = requests.post(
            'https://open.feishu.cn/open-apis/im/v1/files',
            headers={'Authorization': f'Bearer {token}'},
            files={'file': ('audio.ogg', f, 'audio/ogg')},
            data={'file_type': 'opus', 'file_name': 'audio.ogg'}
        )

    upload_data = upload_resp.json()
    if upload_data.get('code') != 0:
        raise ValueError(f"Upload failed: {upload_data}")

    file_key = upload_data['data']['file_key']

    send_resp = requests.post(
        'https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=open_id',
        headers={'Authorization': f'Bearer {token}'},
        json={
            'receive_id': FEISHU_USER_ID,
            'msg_type': 'audio',
            'content': json.dumps({'file_key': file_key})
        }
    )
    return send_resp.json()


def main():
    text = sys.argv[1] if len(sys.argv) > 1 else "测试语音"
    voice = sys.argv[2] if len(sys.argv) > 2 else "Nofish"

    print(f"生成语音: {text[:20]}... 音色: {voice}")

    audio_url = generate_qwen_audio(text, voice)
    print(f"音频URL: {audio_url[:50]}...")

    wav_path = download_wav(audio_url)
    ogg_path = add_silence_and_convert(wav_path)
    print(f"转换完成(已加1.5秒空白): {ogg_path}")

    [REDACTED_SECRET])
    result = send_audio(token, ogg_path)
    print(f"发送结果: {result.get('code')} - {result.get('msg')}")

    os.unlink(ogg_path)


if __name__ == '__main__':
    main()


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