# SixEaredTTS API 接入指南

使用 SixEaredTTS API，你可以上传参考音频、创建音色、提交文字生成语音，再下载 MP3。所有音色和任务都与调用时使用的 API Key 关联。

接入前准备：已开通的 API Key、开通时提供的服务地址，以及一份有使用授权、清晰可辨的参考音频。本文示例使用 Python 3 和 `requests`。

## 快速开始

推荐流程：上传预检 → 复刻音色 → 提交生成任务 → 查询任务 → 下载 MP3。如果参考音频已确认可用，也可以直接复刻，默认会检查音频。

安装依赖，并设置服务地址和 Key。`api.example.com` 是示例域名，请换成开通时提供的服务地址。

```bash
pip install requests
export TTS_API_BASE="https://api.example.com"
export TTS_API_KEY="sk-你的密钥"
```

下面的代码可以直接保存为 `example.py`。将参考音频放在同一目录，命名为 `ref.wav`。首次运行会创建 `demo_voice`；同一 Key 下再次使用这个名称会覆盖同名音色。

```python
import os
import time
from pathlib import Path
from urllib.parse import quote

import requests

BASE = os.environ["TTS_API_BASE"].rstrip("/")
KEY = os.environ["TTS_API_KEY"]
VOICE_ID = "demo_voice"
HTTP = requests.Session()
HTTP.headers["Authorization"] = f"Bearer {KEY}"


def checked(response):
    response.raise_for_status()
    body = response.json()
    if body.get("code") != 0:
        raise RuntimeError(body.get("msg", "请求失败"))
    return body["data"]


with open("ref.wav", "rb") as audio:
    voice = checked(HTTP.post(
        f"{BASE}/api/v1/voices",
        files={"file": ("ref.wav", audio, "audio/wav")},
        data={"name": VOICE_ID, "precheck_gate": "true"},
        timeout=(5, 180),
    ))
print("音色：", voice["voice_id"], "预检分数：", voice["health_score"])

task = checked(HTTP.post(
    f"{BASE}/api/v1/tts/tasks",
    json={"text": "你好，欢迎使用语音生成服务。", "voice_id": voice["voice_id"]},
    timeout=(5, 30),
))
task_id = task["task_id"]
print("任务编号：", task_id)

# 本例最多等待 5 分钟。超时后保留 task_id，可继续查询同一任务。
deadline = time.monotonic() + 300
while time.monotonic() < deadline:
    response = HTTP.get(
        f"{BASE}/api/v1/tts/tasks/{quote(task_id, safe='')}",
        timeout=(5, 15),
    )
    result = checked(response)
    if result["status"] == "success":
        audio = HTTP.get(BASE + result["download_url"], timeout=(5, 60))
        audio.raise_for_status()
        if not audio.headers.get("Content-Type", "").startswith("audio/"):
            raise RuntimeError("返回的不是音频文件")
        Path("result.mp3").write_bytes(audio.content)
        print("已保存 result.mp3")
        break
    time.sleep(3)
else:
    raise TimeoutError(f"等待超时，请继续查询任务 {task_id}")
```

后面的单接口示例复用这里的 `BASE`、`HTTP`、`VOICE_ID` 和 `checked()`。文件上传请使用 `files=`，不要自己设置 multipart 的 `Content-Type`，Python 会自动生成正确的分隔符。

## 鉴权与响应

除 `GET /healthz` 外，所有客户 API 都必须携带下面的请求头。将 `sk-你的密钥` 换成完整 Key。

```http
Authorization: Bearer sk-你的密钥
```

不要把 Key 写到 URL、代码仓库或公开前端代码中。不同 Key 的音色和任务不能互相查询；创建、查询和下载应使用同一把 Key。

JSON 响应统一包含三个字段，字段顺序不固定。

```json
{"code":0,"msg":"ok","data":{}}
```

| 字段 | 类型 | 如何使用 |
| --- | --- | --- |
| code | integer | `0` 表示本次请求成功，其他值须结合 HTTP 状态和下表处理 |
| msg | string | 人类可读说明；可能包含音色名称或具体失败原因，不要用完整文案做程序分支判断 |
| data | object / array / null | 返回数据；音色列表为数组，单条音色、任务、余额和流水为对象，部分错误为 `null` |

一定要同时检查 HTTP 状态和 `code`。HTTP 200 也可能表示预检不合格、音频处理失败或生成任务失败。下载接口成功时返回音频二进制，失败时可能返回 JSON。

## 错误处理

| HTTP | code | 含义 | 建议处理 |
| --- | --- | --- | --- |
| 200 | 0 | 请求成功 | 继续读取 `data`；任务仍需检查 `data.status` |
| 200 | -2 | 音频预检不合格 | 更换清晰参考音频，避免反复提交同一文件 |
| 200 | -1 | 预检执行失败或任务生成失败 | 读取 `msg`；任务为 `failed` 时停止轮询 |
| 400 | -1 | 缺少必填参数、音色名无效、文件或日期不合法 | 修正请求后重试 |
| 401 | -1 | Key 缺失、无效、未开通或已停用 | 核对 Key；仍有问题请联系服务方 |
| 404 | 404 | 音色不存在、任务不存在或已过期、音频无法下载 | 核对编号和 Key；到期音色需要重新复刻 |
| 409 | -1 | 生成音频尚未就绪 | 继续查询任务，等 `success` 再下载 |
| 429 | -1 | 请求频率、App 未完成任务数或音色并发达到上限 | 根据 msg 与 Retry-After 降低提交或查询频率 |
| 500 | -1 | 服务处理失败 | 保留请求时间和编号，联系服务方排查 |
| 503 | -1 | 容量或查询服务暂不可用 | 稍后重试；持续失败请联系服务方 |

Key 错误时的响应示例：

```json
{"code":-1,"msg":"请求非法，不允许请求。","data":null}
```

文件缺失、空文件或超过 5MB 时，返回 HTTP 400：

```json
{"code":-1,"msg":"音频无效","data":null}
```

默认每把 Key 每分钟最多 300 次客户 API 请求，上传、查询和下载共用额度；具体以开通时的调用限制为准。建议单任务每 3 秒查询一次，同时查询多任务时控制总请求数。

提交任务超时或连接中断，不代表请求一定未被接收。当前不支持 `Idempotency-Key`；不要为同一文本自动无限重提，避免重复生成和重复计费。已经拿到 `task_id` 时，继续查询原任务。

## 服务可用性

`GET /healthz`

无需鉴权、无请求参数。用于确认服务能否接收请求。

```python
response = requests.get(f"{BASE}/healthz", timeout=(5, 10))
print(response.status_code, response.json())
```

HTTP 200，响应示例：

```json
{"code":0,"msg":"ok","data":{"ready":true}}
```

HTTP 503 表示暂不可用。此接口成功不代表每次生成都能立即开始，生成还可能需要排队。

## 上传音频预检

`POST /api/v1/voices/precheck`

`Content-Type: multipart/form-data`

检查参考音频能否通过音色预检，返回合格状态和可用的预检分数。这个请求不会创建或保存音色。

| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| file | file | 是 | 参考音频，不能为空，最大 5MB；推荐清晰、单人、少噪声的 WAV |
| name | string | 否 | 本次检查的显示名称，可能出现在 `msg` 中 |

```python
with open("ref.wav", "rb") as audio:
    response = HTTP.post(
        f"{BASE}/api/v1/voices/precheck",
        files={"file": ("ref.wav", audio, "audio/wav")},
        data={"name": VOICE_ID},
        timeout=(5, 180),
    )
print(response.status_code, response.json())
```

合格，HTTP 200，响应示例：

```json
{"code":0,"msg":"音色《demo_voice》预检合格分（99.4）","data":{"passed":true,"health_score":99.4}}
```

不合格，HTTP 200，响应示例：

```json
{"code":-2,"msg":"音色《demo_voice》预检不合格","data":{"passed":false,"health_score":null}}
```

无效音频导致预检执行失败，HTTP 200，响应示例：

```json
{"code":-1,"msg":"快速预检失败，请稍后重试","data":{"passed":false,"health_score":null}}
```

| 返回字段 | 类型 | 说明 |
| --- | --- | --- |
| passed | boolean | 是否合格。以这个字段及 `code` 判断能否继续，不能只按分数自行判断 |
| health_score | number / null | 本次可用的预检分数；未提供分数时为 `null`，不能按 `0` 分解释 |

没有可用容量时 HTTP 503，`msg` 为「暂无可用算力」。

## 复刻音色

`POST /api/v1/voices`

`Content-Type: multipart/form-data`

上传参考音频，创建后续生成要使用的音色。保存返回的 `voice_id`，以后通过它选择音色。

| 字段 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| file | file | 是 | 无 | 参考音频，不能为空，最大 5MB |
| name | string | 否 | 自动生成 | 音色编号，1–64 字符；支持英文字母、数字、下划线、连字符、中文 |
| prompt_text | string | 否 | 空 | 参考音频对应的文本；可以省略，最多 1000 个字符 |
| precheck_gate | string | 否 | `true` | 默认检查合格后才创建；仅字符串 `false` 表示跳过预检 |

同一 Key 下重复使用 `name` 会覆盖同名音色。建议为不同客户声音使用不同名称，覆盖前确认没有正在使用该音色的生成请求。

```python
with open("ref.wav", "rb") as audio:
    response = HTTP.post(
        f"{BASE}/api/v1/voices",
        files={"file": ("ref.wav", audio, "audio/wav")},
        data={"name": VOICE_ID, "precheck_gate": "true"},
        timeout=(5, 180),
    )
print(response.status_code, response.json())
```

成功，HTTP 200，响应示例：

```json
{"code":0,"msg":"音色复刻成功","data":{"voice_id":"demo_voice","voice_name":"demo_voice","passed":true,"health_score":99.4}}
```

预检不合格时不会创建音色，返回 HTTP 200：

```json
{"code":-2,"msg":"音色《demo_voice》预检不合格","data":{"passed":false,"health_score":null}}
```

| 返回字段 | 类型 | 说明 |
| --- | --- | --- |
| voice_id | string | 后续生成、查询和删除使用的音色编号 |
| voice_name | string | 音色名称 |
| passed | boolean | 创建成功时为 `true`；跳过预检时不代表音频经过了检查 |
| health_score | number / null | 创建时的预检分数；跳过预检、未获取分数或历史记录未记分时为 `null` |

HTTP 400 表示音频、名称或复刻请求无效；HTTP 200、`code=-1` 表示检查执行失败；HTTP 503 表示当前暂无可用容量。不要把跳过预检作为低质量音频的默认处理方式。

## 查询音色列表

`GET /api/v1/voices`

无请求参数。返回当前 Key 创建且尚未删除的音色，`data` 是数组；没有音色时为 `[]`。

```python
voices = checked(HTTP.get(f"{BASE}/api/v1/voices", timeout=(5, 15)))
for voice in voices:
    print(voice["voice_id"], voice["health_score"], voice["expires_at"])
```

HTTP 200，响应示例：

```json
{
  "code": 0,
  "msg": "ok",
  "data": [
    {
      "voice_id": "demo_voice",
      "voice_name": "demo_voice",
      "original_name": "ref.wav",
      "health_score": 99.4,
      "created_at": 1793584659,
      "expires_at": 1796176659
    }
  ]
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| voice_id | string | 用于后续请求的音色编号 |
| voice_name | string | 音色显示名称 |
| original_name | string | 上传时的文件名 |
| health_score | number / null | 创建时记录的分数；`null` 表示没有记录 |
| created_at | integer | 创建时间，Unix 秒 |
| expires_at | integer | 当前到期时间，Unix 秒；以返回值为准 |

示例中的保留期为 30 天。保留期可能随服务配置调整，不要在客户端写死；到期前应安排重新复刻。

## 查询单个音色

`GET /api/v1/voices/{voice_id}`

路径参数 `voice_id` 必填，使用复刻成功返回的编号。包含中文时进行 URL 编码。

```python
from urllib.parse import quote

voice = checked(HTTP.get(
    f"{BASE}/api/v1/voices/{quote(VOICE_ID, safe='')}",
    timeout=(5, 15),
))
print(voice)
```

HTTP 200，响应示例：

```json
{"code":0,"msg":"ok","data":{"voice_id":"demo_voice","voice_name":"demo_voice","original_name":"ref.wav","health_score":99.4,"created_at":1793584659,"expires_at":1796176659}}
```

不存在、已删除或 Key 不匹配时，HTTP 404：

```json
{"code":404,"msg":"音色不存在，请重新复刻","data":null}
```

## 下载参考音频

`GET /api/v1/voices/{voice_id}/audio`

路径参数 `voice_id` 必填。下载创建音色时上传的原始参考音频。

```python
response = HTTP.get(
    f"{BASE}/api/v1/voices/{quote(VOICE_ID, safe='')}/audio",
    timeout=(5, 60),
)
response.raise_for_status()
if not response.headers.get("Content-Type", "").startswith("audio/"):
    raise RuntimeError("返回的不是音频文件")
Path("reference.wav").write_bytes(response.content)
```

成功返回 HTTP 200，响应体为原始音频二进制，不带 JSON 包装。WAV 文件的 `Content-Type` 可以是 `audio/x-wav`，具体类型和长度取决于上传的文件。

音色不存在或已删除时返回 HTTP 404，JSON 同单个音色查询。

## 创建语音任务

`POST /api/v1/tts/tasks`

`Content-Type: application/json`

提交要合成的文字和已创建的音色。成功表示任务被接收，请保存 `task_id`，然后查询生成状态。

| 字段 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| text | string | 是 | 无 | 要合成的文字，不能为空 |
| voice_id | string | 是 | 无 | 当前 Key 下已有的音色编号 |
| temperature | number | 否 | 1 | 随机性，建议 0.1–2；不确定时保持默认 |
| top_p | number | 否 | 0.8 | 概率采样参数，建议大于 0 且不超过 1 |
| top_k | integer | 否 | 30 | 候选采样数，建议 1–100 |
| repetition_penalty | number | 否 | 10 | 重复惩罚，建议 1–20 |
| max_tokens | integer | 否 | 768 | 单次生成长度参数，建议 64–768，不等于文字字数 |
| seed | string | 否 | 空字符串 | 随机种子；不固定时省略或传空字符串，使用时传不超过 64 个字符的整数 |

数值参数传 `0` 时，当前会按默认值处理。不要把 `0` 用作关闭开关。参数推荐范围不代表所有异常输入都会在提交时拦截，生成结果仍要通过任务查询确认。

```python
task = checked(HTTP.post(
    f"{BASE}/api/v1/tts/tasks",
    json={
        "text": "你好，这是语音接口测试。",
        "voice_id": VOICE_ID,
        "temperature": 1,
        "top_p": 0.8,
        "top_k": 30,
        "repetition_penalty": 10,
        "max_tokens": 768,
        "seed": "",
    },
    timeout=(5, 30),
))
task_id = task["task_id"]
```

HTTP 200，响应示例：

```json
{"code":0,"msg":"任务已提交","data":{"task_id":"t8caae898d8ede289","status":"queued"}}
```

| 失败情况 | HTTP | code | msg |
| --- | --- | --- | --- |
| 缺少文字、音色编号，或 JSON 格式不正确 | 400 | -1 | 参数无效 |
| 音色不存在、已删除或不属于当前 Key | 404 | 404 | 音色不存在，请重新复刻 |
| 等待任务过多 | 503 | -1 | 队列已满 |
| 暂时不能接收任务 | 503 | -1 | 以实际返回的服务不可用说明为准 |

## 查询生成状态

`GET /api/v1/tts/tasks/{task_id}`

路径参数 `task_id` 必填。建议每 3 秒查询一次。查询必须使用提交任务时相同的 Key。

```python
response = HTTP.get(
    f"{BASE}/api/v1/tts/tasks/{quote(task_id, safe='')}",
    timeout=(5, 15),
)
print(response.status_code, response.json())
```

排队，HTTP 200，响应示例：

```json
{"code":0,"msg":"queued","data":{"task_id":"t8caae898d8ede289","status":"queued","voice_id":"demo_voice"}}
```

完成，HTTP 200，响应示例：

```json
{
  "code": 0,
  "msg": "success",
  "data": {
    "task_id": "t8caae898d8ede289",
    "status": "success",
    "voice_id": "demo_voice",
    "elapsed_s": 0.34,
    "duration_s": 2.816,
    "download_url": "/api/v1/tts/tasks/t8caae898d8ede289/audio"
  }
}
```

| 字段或状态 | 类型 | 含义 |
| --- | --- | --- |
| task_id | string | 本次任务编号 |
| voice_id | string | 使用的音色编号 |
| status=queued | string | 已接收，等待处理 |
| status=running | string | 正在生成 |
| status=success | string | 生成完成，可以下载 |
| status=failed | string | 生成失败，停止轮询并读取 `msg` |
| elapsed_s | number | 有值时表示生成耗时，单位秒，不保证等于客户端等待时间 |
| duration_s | number | 有值时表示音频长度，单位秒 |
| download_url | string | 成功后返回的下载路径；拼接服务地址，并继续携带 Key |

客户端可能直接从 `queued` 看到 `success`，不保证能轮询到 `running`。`elapsed_s`、`duration_s` 只有已获取且大于 0 时返回，不能要求它们在每次响应都存在。

若 `msg` 为「任务提交回执未确认，请核对消费记录后再决定是否重新提交」，表示这次提交的接收结果不确定。请停止自动重试，先核对消费流水；再次提交可能重复生成及扣费。普通任务失败也应根据具体原因决定下一步操作。

生成失败的响应格式如下；以下是结构说明，具体失败原因随任务变化：

```json
{"code":-1,"msg":"音频处理失败，请稍后重试","data":{"task_id":"t8caae898d8ede289","status":"failed","voice_id":"demo_voice"}}
```

任务不存在、已过期或 Key 不匹配，HTTP 404，响应示例：

```json
{"code":404,"msg":"任务不存在或已过期","data":null}
```

默认下载有效期约 1 小时。生成后及时下载并自行保存，不要把 `download_url` 当作长期公开音频地址。需要网页播放时，先用鉴权请求获取音频，再使用下载结果或对象 URL。

## 下载生成音频

`GET /api/v1/tts/tasks/{task_id}/audio`

路径参数 `task_id` 必填。仅在查询到 `status=success` 后下载。完整示例见「快速开始」。

```python
response = HTTP.get(
    f"{BASE}/api/v1/tts/tasks/{quote(task_id, safe='')}/audio",
    timeout=(5, 60),
)
response.raise_for_status()
if not response.headers.get("Content-Type", "").startswith("audio/"):
    raise RuntimeError("返回的不是 MP3 音频")
Path("result.mp3").write_bytes(response.content)
```

成功返回 HTTP 200、`Content-Type: audio/mpeg`，响应体为 MP3 二进制。文件长度随文本和生成结果变化。

尚未完成时，HTTP 409，响应示例：

```json
{"code":-1,"msg":"音频未就绪","data":null}
```

任务不存在或过期返回 HTTP 404、`msg=任务不存在或已过期`；音频无法下载返回 HTTP 404、`msg=音频已不能下载`。两者 `code=404`、`data=null`。

## 删除音色

`DELETE /api/v1/voices/{voice_id}`

路径参数 `voice_id` 必填。删除当前 Key 下指定音色，后续不能继续用这个编号提交生成。

```python
response = HTTP.delete(
    f"{BASE}/api/v1/voices/{quote(VOICE_ID, safe='')}",
    timeout=(5, 60),
)
print(response.status_code, response.json())
```

HTTP 200，响应示例：

```json
{"code":0,"msg":"已删除","data":{"voice_id":"demo_voice"}}
```

删除成功表示该音色已停用，不能再提交新生成；远端清理可能异步完成。音色同时被覆盖或迁移时，可能返回 HTTP 409，请重新查询当前状态后决定是否重试。删除后再次查询，返回 HTTP 404、`code=404`。不存在的音色再次删除也返回 404；不要在生成仍需使用该音色时删除它。

## 查询账户余额与历史消耗

`GET /api/v1/billing/balance`

无请求参数。返回账户可用余额、当前 Key 的历史消耗金额，以及所属账户的累计真实 Token。累计 Token 不受流水页面日期筛选影响。

```python
balance = checked(HTTP.get(f"{BASE}/api/v1/billing/balance", timeout=(10, 60)))
print("账户余额：", balance["balance_usd"])
print("历史消耗金额：", balance["used_usd"])
if balance["total_tokens_available"]:
    print("账户累计 Token：", balance["total_tokens"])
    print("统计获取时间：", balance["total_tokens_as_of"])
else:
    print(balance.get("total_tokens_error", "账户 Token 统计暂不可用"))
```

响应示例（HTTP 200）：

```json
{
  "code": 0,
  "msg": "ok",
  "data": {
    "token_name": "1",
    "balance_usd": 10200.0006,
    "used_usd": 180.061124,
    "total_tokens": 128900,
    "total_tokens_scope": "account",
    "total_tokens_available": true,
    "total_tokens_as_of": 1793603412,
    "expires_at": 0
  }
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| token_name | string | 当前 Key 的显示名称 |
| balance_usd | number / null | 账户可用余额；金额查询失败或响应无效时为 `null`；`0` 表示已确认的零余额，负数表示账户欠费 |
| used_usd | number | 当前 Key 累计历史消耗金额 |
| total_tokens | integer / null | 所属账户的累计真实 Token；不是金额或额度点数 |
| total_tokens_scope | string | 固定为 `account`，表示账户统计 |
| total_tokens_available | boolean | 本次能否取得有效账户 Token 统计；不可用时 `total_tokens=null`，不是 0 |
| total_tokens_as_of | integer / null | 本次统计从上游获取的时间，Unix 秒；不是最后一次消费发生时间。缓存期内保持不变 |
| total_tokens_error | string | 仅统计不可用时返回说明；余额和历史金额查询可独立成功 |
| expires_at | integer | Key 到期时间，Unix 秒；`0` 表示长期有效 |

查询失败或无法解析有效消耗数据时返回 HTTP 503、`msg=查询暂不可用`；Key 被拒绝返回 HTTP 401。失败数据不会以零消耗显示。

## 按日期查询流水

`GET /api/v1/billing/logs`

按分页拉取指定日期范围内的消费记录，并返回区间汇总与账户趋势数据。日期按北京时间（`Asia/Shanghai`）计算，包含开始日和结束日。

流水与区间金额按账户和令牌名称查询（`query_scope=account_token_name`），趋势数据按账户统计（`series_scope=account`）。

| 参数 | 类型 | 必填 | 默认 | 说明 |
| --- | --- | --- | --- | --- |
| start_date | string | 是 | 无 | `YYYY-MM-DD`，开始日 00:00:00 |
| end_date | string | 是 | 无 | `YYYY-MM-DD`，结束日 23:59:59；不能早于开始日 |
| page | integer | 否 | 1 | 从 1 开始；小于 1 或无法解析时使用 1 |
| page_size | integer | 否 | 20 | 1–100；超出范围或无法解析时使用 20 |
| type | integer | 否 | 2 | `2` 为消费；其他类型编号见下表 |
| skip_stats | integer | 否 | 0 | 传 `1` 时仅按页实时拉取当前页列表与总条数 `total`，不重复计算趋势序列 |

单次最多查询 31 个自然日。查询更长时间请拆成多个区间，避免重复包含边界日期。

| type | 返回的类型名称 |
| --- | --- |
| 1 | 充值 |
| 2 | 消费 |
| 3 | 管理 |
| 4 | 系统 |
| 5 | 错误 |
| 6 | 退款 |

```python
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo

today = datetime.now(ZoneInfo("Asia/Shanghai")).date()
response = HTTP.get(
    f"{BASE}/api/v1/billing/logs",
    params={
        "start_date": (today - timedelta(days=6)).isoformat(),
        "end_date": today.isoformat(),
        "page": 1,
        "page_size": 20,
        "type": 2,
    },
    timeout=(10, 30),
)
print(response.status_code, response.json())
```

成功响应示例：

```json
{
  "code": 0,
  "msg": "ok",
  "data": {
    "page": 1,
    "page_size": 20,
    "total": 20119,
    "query_scope": "account_token_name",
    "period_usd": 4.83012,
    "period_count": 20119,
    "series_scope": "account",
    "series_available": true,
    "series_usd": 6.48,
    "series_count": 22000,
    "items": [
      {
        "created_at": 1793603412,
        "type": 2,
        "type_name": "消费",
        "model_name": "cloud_light_tts",
        "cost_usd": 0.00024,
        "prompt_tokens": 4,
        "completion_tokens": 0,
        "use_time": 0
      }
    ],
    "daily_series": [
      {
        "label": "11-02",
        "usd": 1.75692,
        "count": 7316
      }
    ],
    "hourly_series": [
      {
        "label": "11-02 14:00",
        "usd": 0.0576,
        "count": 240
      }
    ]
  }
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| page / page_size | integer | 当前页码和每页条数 |
| total | integer | 上游按相同账户、令牌名称、日期及类型条件查询的总记录数，不是当前页条数；实时数据变化时可变 |
| query_scope | string | `account_token_name`：流水与区间汇总按账户和令牌名称过滤 |
| period_usd | number | 所选日期区间累计消耗金额（`skip_stats=1` 时省略） |
| period_count | integer | 所选日期区间累计调用次数（`skip_stats=1` 时省略） |
| items | array | 当前页实时拉取的记录列表；没有记录时为 `[]` |
| items[].created_at | integer | 记录时间，Unix 秒 |
| items[].type / type_name | integer / string | 记录类型编号及显示名称 |
| items[].model_name | string | 计费模型名称 |
| items[].cost_usd | number | 本条记录消耗金额 |
| items[].prompt_tokens / completion_tokens | number | 本条记录的输入 / 输出 Token 计数，以计费模型上报为准；不是额度点数 |
| items[].use_time | number | 记录中的用时，单位秒 |
| series_scope | string | `account`：趋势按账户统计；`skip_stats=1` 时省略 |
| series_available | boolean | 能否取得账户趋势。为 `false` 时数组为空，不能解释为账户消耗为 0；`skip_stats=1` 时省略 |
| series_usd / series_count | number / integer | 账户日趋势覆盖区间的金额 / 调用数，与 Key 范围的 `period_usd`、`period_count` 不同；趋势不可用时不应展示为真实零值；`skip_stats=1` 时省略 |
| daily_series | array | 所选区间的账户按日趋势，每项含 `label`、`usd`、`count`（`skip_stats=1` 时省略） |
| hourly_series | array | 所选区间内最近 48 小时的账户趋势，每项含 `label`、`usd`、`count`（`skip_stats=1` 时省略） |

| 情况 | HTTP | code | msg |
| --- | --- | --- | --- |
| 日期缺失、格式不正确或结束日早于开始日 | 400 | -1 | 日期无效 |
| 超过 31 个自然日 | 400 | -1 | 日期跨度不能超过 31 天 |
| 流水查询未开通 | 503 | -1 | 流水查询未配置 |
| 流水服务故障或查询权限失效 | 503 | -1 | 流水查询服务不可用，请联系管理员 |
| 区间金额汇总不可用 | 503 | -1 | 消费汇总暂不可用，请稍后重试 |

任务查询读取后台持续更新的状态，不会因更密集查询而加快生成。建议每 3 秒查询一次，遇到 429 请降低频率，稍后重试。

## 常见问题

「音色列表为什么是空的？」请确认使用创建音色时的同一 Key，且音色没有被删除或到期。独立上传预检不会创建音色。

「有分数就一定合格吗？」请以 `code` 和 `passed` 为准。`health_score=null` 表示未获取分数，并不等于 0 分。

「返回 200 为什么还失败？」HTTP 200 只表示这次请求有正常响应；预检不合格和生成失败可能通过 `code` 表达。

「为什么看不到生成中？」短任务可能在两次轮询之间完成。看到 `success` 即可下载，不必等待曾经出现 `running`。

「任务提交后可以立即播放吗？」先查询到 `success`，再携带 Key 下载。下载路径不是公开、长期有效的播放链接。
