# Long Text Tts

> Long Text Tts

- Skill: `qq5855144/long-text-tts` (Agent Skill, multi-file: 3 files)
- Install (CLI): `npx skillmds@latest add qq5855144/long-text-tts`
- Raw SKILL.md: https://api.skillmd.com/api/skills/qq5855144/long-text-tts/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- License: MIT
- Author: qq5855144 (https://skillmd.com/u/qq5855144)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/qq5855144/long-text-tts

---


## 能力概述

百度 AI 长文本语音合成服务，通过两步异步接口将超长文本转换为音频：

1. **创建任务**（`POST /rpc/2.0/tts/v1/create`）：提交文本数组，返回 `task_id`
2. **查询任务**（`POST /rpc/2.0/tts/v1/query`）：轮询任务状态，成功后返回 `speech_url` 音频链接

| 能力维度 | 说明 |
|----------|------|
| 文本上限 | 总字数不超过 10 万字符 |
| 音频格式 | mp3-16k（默认）、mp3-48k、wav |
| 音库选项 | 度小美(0)、度小宇(1)、度逍遥(3)、度丫丫(4) |
| 语速/音调/音量 | 均为 0–15，默认均为 5 |
| 任务状态 | Running / Success / Failure / Created |
| 结果有效期 | 音频下载链接储存 72 小时 |
| 平台支持 | Web、MiniProgram |

**平台差异关键说明：**

| 差异点 | Web | MiniProgram |
|--------|-----|-------------|
| 音频播放方式 | `<audio>` 标签或 `Audio` 对象，直接使用 Supabase Storage 公开 URL | Taro `InnerAudioContext`，使用 https 链接（须将 http 升级为 https） |
| 轮询计数器实现 | `useRef` 防止重复轮询链 | 同 Web，必须用 `useRef` 而非 `useState` |
| 缓存策略 | 优先读 Supabase DB 缓存的 `speech_url`，避免重复调用 | 同 Web |

---

## 生成期用法（Agent 直接调用）

完整异步工作流：提交任务 → 轮询直到成功/失败。详见 `references/tts-create-api.md`（提交接口）和 `references/tts-query-api.md`（查询接口）。

```typescript
// 完整异步工作流：submit → poll → result
const apiKey = process.env["INTEGRATIONS_API_KEY"]!; // platform_managed，密钥由平台注入

async function createTTSTask(
  text: string[],
  options?: {
    format?: "mp3-16k" | "mp3-48k" | "wav";
    voice?: 0 | 1 | 3 | 4;
    speed?: number;
    pitch?: number;
    volume?: number;
    break?: number;
  }
): Promise<string> {
  const body: Record<string, unknown> = { text };
  if (options?.format !== undefined) body.format = options.format;
  if (options?.voice !== undefined) body.voice = options.voice;
  if (options?.speed !== undefined) body.speed = options.speed;
  if (options?.pitch !== undefined) body.pitch = options.pitch;
  if (options?.volume !== undefined) body.volume = options.volume;
  if (options?.break !== undefined) body.break = options.break;

  const response = await fetch(
    "https://app-bo4w33bsdqm9-api-nYWNozBb8X3L-gateway.appmiaoda.com/rpc/2.0/tts/v1/create",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Gateway-Authorization": `Bearer ${apiKey}`,
      },
      body: JSON.stringify(body),
    }
  );
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  const json = await response.json();
  if (!json.task_id) throw new Error(`Create failed: ${JSON.stringify(json)}`);
  return json.task_id as string;
}

async function queryTTSTask(taskId: string): Promise<{
  task_status: "Success" | "Running" | "Failure" | "Created";
  speech_url?: string;
}> {
  const response = await fetch(
    "https://app-bo4w33bsdqm9-api-Q9KWZ2jy8W09-gateway.appmiaoda.com/rpc/2.0/tts/v1/query",
    {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        "X-Gateway-Authorization": `Bearer ${apiKey}`,
      },
      body: JSON.stringify({ task_ids: [taskId] }),
    }
  );
  if (!response.ok) throw new Error(`HTTP error: ${response.status}`);
  const json = await response.json();
  const taskInfo = json.tasks_info?.[0];
  if (!taskInfo) throw new Error("No task info returned");
  return {
    task_status: taskInfo.task_status,
    speech_url: taskInfo.task_result?.speech_url,
  };
}

async function generateLongTextTTS(
  text: string[],
  options?: Parameters<typeof createTTSTask>[1]
): Promise<string> {
  const taskId = await createTTSTask(text, options);

  const POLL_INTERVAL_MS = 7000;
  const TIMEOUT_MS = 10 * 60 * 1000; // 10 分钟
  const deadline = Date.now() + TIMEOUT_MS;

  while (Date.now() < deadline) {
    await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
    const result = await queryTTSTask(taskId);
    if (result.task_status === "Success" && result.speech_url) {
      return result.speech_url;
    }
    if (result.task_status === "Failure") {
      throw new Error(`TTS task failed: ${taskId}`);
    }
    // Running / Created → 继续轮询
  }
  throw new Error(`TTS task ${taskId} timed out after 10 minutes`);
}
```

---

## 生成后用法（应用内通过 Edge Function 调用）

需要两个 Edge Function：`tts-create`（提交任务）和 `tts-query`（查询结果）。
- `tts-create` 的完整代码及前端调用见 `references/tts-create-api.md`
- `tts-query` 的完整代码及前端调用（含轮询、缓存、错误处理）见 `references/tts-query-api.md`

**前端整体架构（Web & MiniProgram 通用）：**

1. 调用 `tts-create` Edge Function 提交任务，获取 `task_id`
2. 将 `task_id` 存入 Supabase DB（状态 `Running`）
3. 使用 `useRef` 计数器轮询 `tts-query`，避免双重轮询链
4. 任务成功后，Edge Function 将 `speech_url` 转存至 Supabase Storage，返回永久 `publicUrl`
5. 将 `publicUrl` 更新至 DB（状态 `Success`），前端展示播放控件

> 下次加载时优先从 DB 读取 `speech_url`；如已有完成记录，直接使用缓存 URL，跳过轮询。

---

## 注意事项

- **密钥安全**：`INTEGRATIONS_API_KEY` 仅在 Edge Function 服务端使用，严禁暴露到前端
- **计费**：每次调用 `tts-create` 计费，原价 ¥13.5/千次，折扣价 ¥11.25/千次；`tts-query` 不计费
- **错误处理**：务必处理 429（配额超限）和 402（余额不足）；Baidu TTS `error_code: 18` 表示 QPS 超限，需退避重试
- **音频链接有效期**：`speech_url` 仅保留 72 小时，应在 Edge Function 侧及时转存至 Supabase Storage
- **MiniProgram 特别说明**：原始 `speech_url` 使用 http，微信小程序会阻断非 https 资源，需升级协议或使用 Supabase Storage 的 https URL

