# AuralWise API 参考文档

REST API，支持语音转写、说话人分离、声音事件检测，可直接集成到任何语言的应用中。

**BASE URL:** `https://api.auralwise.cn/v1`

---

## 简介

AuralWise 将 GPU 密集型音频处理能力封装为标准 HTTP API。提交音频后，系统异步处理并将结果存储，你可以轮询状态或通过回调获取结果。

典型场景：

| 场景 | ASR（转写） | 说话人分离 | 声音事件 |
|------|:-----------:|:----------:|:--------:|
| 完整处理 | ✅ | ✅ | ✅ |
| 仅转写 | ✅ | ✕ | ✕ |
| 仅事件检测 | ✕ | ✕ | ✅ |
| 转写 + 分离 | ✅ | ✅ | ✕ |

处理流程：

```
提交音频 → 轮询状态 → processing → done → 获取结果
```

---

## 转写模式对比

系统提供两档转写：**优化档**（省成本）与**标准档**（高质量），通过 `optimize` 参数和检测到的音频语言自动路由。理解差异有助于选择最适合你场景的配置。

### 优化档（省成本）

- **触发条件：** `optimize=true`（适用语言）
- **处理速度：** 极快 —— 批量推理，比标准档快 10–30 倍
- **计费：** 更低 —— 优化档单价远低于标准档，具体价格见定价页
- **适用语言：** 中文/英/西/法/葡 —— 其他语言自动使用标准档
- **时间戳精度：** 段级 ~100ms —— 仅 `segments[].start/end`，无 `words` 字段
- **适用场景：** 正文级精度 —— 检索/摘要/听写；容忍专有名词精度略降

### 标准档（高质量）

- **触发条件：** `optimize=false` 或语言不适用优化档
- **识别质量：** 更高 —— 中英混读、专有名词、品牌名识别更准
- **计费：** 标准基础价格
- **时间戳精度：** 词/字级 ~40ms —— `segments[].words[]` 含每个词/字的 `start/end`
- **适用语言：** 全语言 —— 中/英/日/韩/法/德/西/俄/阿等

### 如何选择？

- **默认（不传 optimize）：** 按语言自动判断——中文及英/西/法/葡默认走优化档（省成本），其余走标准档。
- **需要词/字级时间戳**（字幕、卡拉 OK 对齐）：设 `optimize=false` 走标准档，获取每个词/字的精确时间戳。
- **中英混读、专有名词/品牌名重要：** 设 `optimize=false`，标准档识别更准。
- **极致省成本、只需正文：** 设 `optimize=true`（仅对适用语言生效，其余仍走标准档）。

### 路由逻辑

```
音频提交 → 语言识别 → optimize？语言适用？ → 优化档（段级/省成本） 或 标准档（词级/高质量）
```

---

## 认证

所有请求需在 HTTP Header 中携带 API Key：

```
X-API-Key: asr_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

> API Key 在 **账号设置 → API Key 管理** 中创建。密钥格式为 `asr_` 开头的 44 位字符串，创建后完整内容仅显示一次。

---

## 创建任务

```
POST /tasks
Content-Type: application/json
```

提交音频 URL 或 Base64 编码音频创建一个转写任务。服务端收到请求后立即返回任务 ID，实际处理异步完成。

> **音频文件限制：** URL 模式文件大小 1 KB ~ 2 GB；Base64 模式单次请求最大 200 MB；音频时长最长 5 小时。超出限制的请求将被拒绝或任务标记为失败。

### 请求体字段

| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| audio_url | string | 可选 | 音频文件的公开可访问 URL；audio_url 与 audio_base64 二选一必填。文件大小 1 KB ~ 2 GB，时长不超过 5 小时 |
| audio_base64 | string | 可选 | Base64 编码的音频文件内容（支持标准或 URL-safe 编码，单次请求最大 200 MB）；audio_url 与 audio_base64 二选一必填。时长不超过 5 小时 |
| audio_filename | string | 可选 | 文件名（含扩展名）；audio_base64 模式下必填（用于确定扩展名），audio_url 模式下可选（默认取 URL 末段） |
| options | object | 可选 | 处理选项，所有字段均有默认值，可只传需覆盖的字段，详见任务选项完整参考 |
| callback_url | string | 可选 | 任务进入终态（done/failed/abandoned）后，系统向此 URL 发送 POST 回调 |
| callback_secret | string | 可选 | 设置后为每次回调附带 X-Signature: sha256=HMAC-SHA256(secret, body) 头 |
| batch_mode | boolean | 可选 | 批量模式，适合对完成时间无严格要求的大批量转写，处理可能延迟最多 24 小时。批量模式支持在单个请求中提交多个 audio_url（见「批量提交任务」端点） |
| allow_training | boolean | 可选 | 是否允许本任务的数据用于未来的模型训练改进，默认 false（不参与）。该参数不影响转写结果与缓存 |

### 完整示例

```json
// ── 方式一：音频 URL ──────────────────────────────────────────────────
{
  "audio_url": "https://example.com/episode.mp3",
  "audio_filename": "episode.mp3",        // 可选，用于任务列表显示

  // ── 方式二：Base64 编码音频 ────────────────────────────────────────────
  // "audio_base64": "<base64-encoded-audio-data>",
  // "audio_filename": "episode.mp3",     // 必填，用于确定文件扩展名

  // ── 任务选项（所有字段均有默认值，可只传需修改的字段）────────────────
  "options": {
    // 功能开关
    "enable_asr": true,             // 语音转写（含自动语音段切分）
    "enable_diarize": true,         // 说话人分离（需同时启用 ASR）
    "enable_audio_events": true,    // 声音事件检测（521 类）

    // ASR 参数
    "asr_language": null,           // null=自动检测；或指定如 "zh"、"en"、"ja"
    "optimize": null,               // 转写档位：null=按语言自动；true=优化档(省成本/段级,适用中文及en/es/fr/pt)；false=标准档(高质量/词或字级)
    "timestamp_level": "word",      // 时间戳粒度：word(词/字级)/segment(段级更省)；优化档恒为段级
    "asr_beam_size": 5,             // Beam Search 宽度，越大越准但越慢
    "asr_temperature": 0.0,         // 解码温度，0=贪心解码，>0 启用采样
    "hotwords": null,               // 热词列表，如 ["产品名", "人名"]，提升识别率
    "initial_prompt": null,         // 初始提示文本，可引导转写风格

    // VAD 参数
    "vad_threshold": 0.35,          // 语音概率阈值（0-1），越低越灵敏
    "vad_min_speech_ms": 250,       // 最短语音段时长（毫秒），短于此被忽略
    "vad_min_silence_ms": 100,      // 最短静音间隔（毫秒），短于此不切断
    "vad_speech_pad_ms": 30,        // 语音段前后填充时长（毫秒）
    "no_speech_threshold": 0.6,     // 无语音概率阈值，高于此丢弃片段

    // 说话人分离参数
    "num_speakers": null,           // null=自动检测；或指定固定说话人数（整数）
    "min_speakers": 1,              // 自动检测时最少说话人数
    "max_speakers": 10,             // 自动检测时最多说话人数
    "diarize_min_segment_sec": 0.5,            // 提取声纹所需最短段时长（秒）
    "diarize_single_speaker_threshold": 0.05,  // Silhouette Score 低于此值判为单说话人

    // 声音事件参数
    "audio_events_threshold": 0.3,  // 声音事件置信度阈值，超过此值才输出
    "audio_events_classes": null    // null=检测全部 521 类；或指定如 ["Cough","Music"]
  },

  // ── 批量模式（可选）──────────────────────────────────────────────────
  "batch_mode": false,                                  // 设为 true 时进入批量队列，处理可能延迟最多 24 小时

  // ── 训练授权（可选）──────────────────────────────────────────────────
  "allow_training": false,                              // 是否允许本任务数据用于未来模型训练，默认 false；不影响结果与缓存

  // ── 回调（可选）──────────────────────────────────────────────────────
  "callback_url": "https://your-server.com/webhook",   // 任务终态后 POST 回调
  "callback_secret": "your-hmac-secret"                // 设置后附带 HMAC-SHA256 签名
}
```

### 响应 201 Created

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "processing",
  "audio_filename": "episode.mp3",
  "audio_source_type": "url",
  "audio_size": 52428800,
  "options": { "enable_asr": true, "enable_diarize": true, ... },
  "created_at": "2026-03-09T10:00:00Z",
  "updated_at": "2026-03-09T10:00:00Z"
}
```

> 任务创建后立即进入处理流程，实际处理时间取决于音频时长和当前负载。

> 批量模式适合对完成时间无严格要求的大批量转写作业，处理可能延迟最多 24 小时。批量模式还支持在单个请求中提交多个 audio_url，见下方「批量提交任务」端点。

---

## 批量提交任务

```
POST /tasks/batch
Content-Type: application/json
```

在单个请求中提交多个转写任务，避免频繁调用触发速率限制。仅在 `batch_mode=true` 时可用；仅 `audio_url` 与 `audio_filename` 支持数组化，其余参数（`options`/`callback_url`/`callback_secret`）为所有任务共享。单次请求最多 100 条 URL。

> 逐条独立成功/失败：某条 URL 校验失败或余额耗尽不影响其他条目，已成功创建的任务予以保留。响应按条返回每个任务的成功状态或失败原因。

### 请求体

```json
// 仅 batch_mode=true 时可用；仅 audio_url / audio_filename 数组化
{
  "audio_url": [
    "https://example.com/ep1.mp3",
    "https://example.com/ep2.mp3"
  ],
  "audio_filename": ["ep1.mp3", "ep2.mp3"],   // 可选；提供时长度须与 audio_url 一致
  "options": { "language": "zh" },             // 共享
  "callback_url": "https://your-server.com/webhook",  // 共享，可选
  "batch_mode": true                           // 必须为 true
}
```

### 响应 201 Created

```json
// 201 Created —— 逐条独立成功/失败
{
  "results": [
    {
      "index": 0,
      "audio_url": "https://example.com/ep1.mp3",
      "success": true,
      "task": { "id": "550e8400-...", "status": "processing", ... }
    },
    {
      "index": 1,
      "audio_url": "https://example.com/ep2.mp3",
      "success": false,
      "error": "insufficient balance"
    }
  ],
  "created_count": 1,
  "failed_count": 1
}
```

---

## 列出任务

```
GET /tasks
```

获取当前用户的任务列表，支持按状态过滤和分页。

### 查询参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| status | string | 可选 | 按状态过滤：processing / done / failed / abandoned |
| page | integer | 可选 | 页码，从 1 开始，默认 1 |
| page_size | integer | 可选 | 每页条数，最大 100，默认 20 |

### 响应 200 OK

```json
{
  "tasks": [
    {
      "id": "550e8400-...",
      "status": "done",
      "audio_filename": "episode.mp3",
      "audio_source_type": "url",
      "created_at": "2026-03-09T10:00:00Z",
      "updated_at": "2026-03-09T10:05:30Z",
      "finished_at": "2026-03-09T10:05:30Z"
    }
  ],
  "total": 42,
  "page": 1,
  "page_size": 20
}
```

---

## 查询任务状态

```
GET /tasks/:id
```

获取单个任务的状态。可通过轮询该接口（建议间隔 3-5 秒）等待任务完成。

### 响应 200 OK

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "processing",
  "audio_filename": "episode.mp3",
  "audio_source_type": "url",
  "audio_size": 52428800,
  "audio_md5": "d41d8cd98f00b204e9800998ecf8427e",  // 原始音频 MD5（任务进入处理阶段后可见），可用于客户端去重
  "allow_training": false,      // 回显创建时的训练授权选择
  "error_message": null,
  "options": { ... },
  "created_at": "2026-03-09T10:00:00Z",
  "updated_at": "2026-03-09T10:01:30Z",
  "finished_at": null
}
```

### 任务状态说明

| 状态 | 说明 |
|------|------|
| processing | 正在处理中，可轮询此接口等待完成 |
| done | 处理完成，可通过 /result 接口获取结果 |
| failed | 处理失败，error_message 中包含原因，将自动重试 |
| abandoned | 多次重试失败后放弃，不再重试，可提交新任务 |

---

## 获取任务结果

```
GET /tasks/:id/result
```

获取已完成任务的完整结果。仅当任务状态为 `done` 时返回数据，否则返回 404。

各能力字段仅在该能力启用时出现，未启用的字段不包含在响应中（而非 null），可通过 key 是否存在来判断。

### 响应 200 OK

```json
{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "audio_duration": 315.5,      // 音频总时长（秒）
  "audio_md5": "d41d8cd98f00b204e9800998ecf8427e",  // 原始音频 MD5（hex 32 字符），客户端去重用

  // ─── ASR（enable_asr=true 时包含）───────────────────────
  "language": "zh",
  "language_probability": 0.99,
  "segments": [
    {
      "id": 0,
      "start": 0.500,           // 秒，以音频起始点为零点
      "end": 2.340,
      "text": "这是第一句话",
      "speaker": "SPEAKER_0",   // 仅 enable_diarize=true 时包含
      "words": [                 // 词/字级时间戳（仅标准档，optimize=false 或语言不适用优化档时返回）
        { "word": "这是", "start": 0.500, "end": 0.820, "probability": 0.98 },
        { "word": "第一", "start": 0.840, "end": 1.120, "probability": 0.97 },
        { "word": "句话", "start": 1.140, "end": 1.460, "probability": 0.96 }
      ]
      // 优化档（optimize=true 且语言适用，如中文/英/西/法/葡）时 words 字段不存在，仅有 start/end/text
    }
  ],

  // ─── VAD（enable_asr=true 时包含）──────────────────────
  "vad_segments": [
    { "start": 0.500, "end": 2.340 }
  ],

  // ─── 说话人分离（enable_diarize=true 时包含）────────────
  "num_speakers": 2,
  "speaker_embeddings": [
    {
      "speaker_id": "SPEAKER_0",
      "embedding": [0.124, -0.083, 0.211, "..."],  // 192 维声纹向量
      "segment_count": 25                           // 参与聚类的语音段数
    },
    {
      "speaker_id": "SPEAKER_1",
      "embedding": [-0.057, 0.193, -0.142, "..."],
      "segment_count": 18
    }
  ],
  "diarize_segments": [
    { "start": 0.500, "end": 2.340, "speaker": "SPEAKER_0" }
  ],

  // ─── 声音事件（enable_audio_events=true 时包含）─────────
  // 注意：时间戳精度约 1s（声音事件检测引擎固有限制）
  "audio_events": [
    { "start": 15.0, "end": 15.96, "class": "Cough", "confidence": 0.87 }
  ],

  // ─── 计费明细（任务完成后始终包含）──────────────────────
  "billing": {
    "mode": "standard",            // standard（标准档）/ zh_lite（优化档）
    "billable_minutes": 6,         // 计费分钟数（最小 1，向上取整）
    "detail": {
      "base_price_per_hour": 1.5,
      "base_amount": 0.15,
      "diarize_price_per_hour": 0.3,   // 仅启用说话人分离时出现
      "diarize_amount": 0.03,
      "events_price_per_hour": 0.2,    // 仅启用声音事件检测时出现
      "events_amount": 0.02,
      "subtotal": 0.20,                // 折扣前小计
      "batch_discount": 0,             // 仅批量任务出现，0 表示无折扣
      "total": 0.20                    // 折扣后最终金额，等于 amount
    },
    "amount": 0.20,                // 本任务实际计费金额（元）
    "balance": 48.50               // 账户当前余额（元）
  }
}
```

### 结果字段说明

#### 顶层字段（始终包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| task_id | string | 任务 UUID |
| audio_duration | float | 音频总时长，单位秒 |
| audio_md5 | string | 原始音频文件的 MD5（hex，32 字符），可用于客户端去重 |

#### ASR 字段（enable_asr=true 时包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| language | string | 检测到的音频语言代码，如 zh、en |
| language_probability | float | 语言识别置信度，0～1 |
| segments | object[] | 转写结果，按句子分段，见下方 segments[] 字段说明 |

#### segments[] — 转写段落

| 字段 | 类型 | 说明 |
|------|------|------|
| id | int | 段落序号，从 0 开始 |
| start | float | 段落开始时间，单位秒，以音频起始点为零点 |
| end | float | 段落结束时间，单位秒 |
| text | string | 该段落的转写文本 |
| speaker | string | 说话人 ID（如 SPEAKER_0），仅 enable_diarize=true 时包含 |
| words | object[] | 词/字级时间戳列表（见下方）。**仅标准档（optimize=false 或语言不适用优化档）时返回**；优化档（段级）下此字段不存在，仅提供段级 start/end。也可用 `timestamp_level=segment` 显式仅取段级。 |

#### segments[].words[] — 词级/字级时间戳（标准档专有）

| 字段 | 类型 | 说明 |
|------|------|------|
| word | string | 该词/字的文本内容 |
| start | float | 词开始时间，单位秒 |
| end | float | 词结束时间，单位秒 |
| probability | float | 该词的 ASR 识别置信度，0～1 |

#### VAD 字段（enable_asr=true 时包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| vad_segments | object[] | 语音活动检测（VAD）识别到的语音段列表 |
| vad_segments[].start | float | 语音段开始时间，单位秒 |
| vad_segments[].end | float | 语音段结束时间，单位秒 |

#### 说话人分离字段（enable_diarize=true 时包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| num_speakers | int | 聚类检测到的说话人总数 |
| speaker_embeddings | object[] | 每个说话人的声纹信息 |
| speaker_embeddings[].speaker_id | string | 说话人标识，如 SPEAKER_0、SPEAKER_1 |
| speaker_embeddings[].embedding | float[] | 192 维说话人声纹向量，可用于跨任务说话人比对 |
| speaker_embeddings[].segment_count | int | 该说话人参与聚类的语音段数量，越多代表样本越充分 |
| diarize_segments | object[] | 按说话人标注的时间段列表 |
| diarize_segments[].start | float | 分离段开始时间，单位秒 |
| diarize_segments[].end | float | 分离段结束时间，单位秒 |
| diarize_segments[].speaker | string | 该时间段归属的说话人 ID |

#### 声音事件字段（enable_audio_events=true 时包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| audio_events | object[] | 检测到的声音事件列表，按时间排列 |
| audio_events[].start | float | 事件开始时间，单位秒（精度约 ±1s，受声音事件检测引擎帧步长限制） |
| audio_events[].end | float | 事件结束时间，单位秒 |
| audio_events[].class | string | 声音事件英文类名（如 Cough、Music），共 521 种，可通过 GET /audio-event-classes 查询完整列表 |
| audio_events[].confidence | float | 置信度得分，0～1，超过 audio_events_threshold 的事件才会出现 |

#### 计费明细 billing（任务完成后始终包含）

| 字段 | 类型 | 说明 |
|------|------|------|
| billing.mode | string | 计费档位：standard（标准档）或 zh_lite（优化档；zh_lite 为历史代码名，涵盖中文及英/西/法/葡） |
| billing.billable_minutes | float | 计费分钟数（最小 1 分钟，向上取整） |
| billing.detail | object | 计费分项快照，见下方说明 |
| billing.amount | float | 本任务实际计费金额（元） |
| billing.balance | float | 账户当前余额（元） |

| billing.detail | 类型 | 说明 |
|------|------|------|
| base_price_per_hour | float | 基础单价（元/小时），按 mode 取标准档或优化档单价 |
| base_amount | float | 基础转写费用（元） |
| diarize_price_per_hour | float | 说话人分离单价，仅启用时出现 |
| diarize_amount | float | 说话人分离费用，仅启用时出现 |
| events_price_per_hour | float | 声音事件检测单价，仅启用时出现 |
| events_amount | float | 声音事件检测费用，仅启用时出现 |
| subtotal | float | 折扣前小计（元） |
| batch_discount | float | 批量折扣系数，仅批量任务出现（0 表示无折扣） |
| total | float | 折扣后最终金额（元），等于 billing.amount |

### 时间戳精度说明

| 能力 | 精度 | 说明 |
|------|------|------|
| ASR 段级 | ~10ms | 与语音段边界对齐，消除长音频漂移 |
| VAD | ~10ms | 语音活动检测帧级精度 |
| 说话人分离 | ~10ms | 直接使用 VAD 段边界，与 VAD 精度一致 |
| 声音事件 | ~1s | 声音事件检测以 1s 为步长推理，固有限制，无法通过后处理提升 |

---

## 删除任务

```
DELETE /tasks/:id
```

删除任务及其关联的音频文件和结果文件。处于 `processing` 状态的任务**无法删除**，将返回 409 Conflict。

### 响应 200 OK

```json
{
  "message": "task deleted"
}
```

---

## 账户信息

```
GET /account
```

返回当前账户的余额、可用额度、并发与请求速率限制等信息。客户端可据此**自适应调整运行策略**——例如按 `tps_limit` 计算提交间隔、按 `available_concurrency` 决定一次并发提交几个任务、按 `available_balance` 预留额度，从而避免触发 402 / 429。

> 建议在批量提交前先查一次本接口；数据实时计算，不缓存。

### 响应 200 OK

```json
{
  "balance": 100.0000,               // 余额（元）
  "held_amount": 3.2500,             // 处理中任务预占（冻结）金额
  "resource_pack_equivalent": 0.0,   // 生效资源包剩余等值额度
  "available_balance": 96.7500,      // 可用额度 = balance + pack - held
  "tier": 3,                         // 账户等级
  "concurrency_limit": 10,           // 生效并发任务数上限
  "active_tasks": 2,                 // 当前活跃（处理中）非批量任务数
  "available_concurrency": 8,        // 还可提交的非批量任务数
  "waiting_batch_tasks": 5,          // 处理中（含排队等待）的批量任务数
  "tps_limit": 2.0,                  // API 速率上限（次/秒）
  "tps_burst": 12                    // 突发容量
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| balance | number | 账户余额（元），可能为负 |
| held_amount | number | 当前被处理中任务预占（冻结）的金额；任务完成结算、失败释放 |
| resource_pack_equivalent | number | 生效资源包剩余分钟折算的等值额度（可抵扣任务费用；批量任务不适用） |
| available_balance | number | 可用额度 = balance + resource_pack_equivalent − held_amount，是能否提交任务的实际闸门（不足返回 402） |
| tier | integer | 账户等级（1–5） |
| concurrency_limit | integer | 生效并发任务数上限（取账户等级默认值与资源包加成中的较高者） |
| active_tasks | integer | 当前活跃（pending/processing）的非批量任务数 |
| available_concurrency | integer | 还可提交的非批量任务数 = max(0, concurrency_limit − active_tasks)；为 0 时再提交将返回 429 |
| waiting_batch_tasks | integer | 当前处于处理中（含排队等待时间窗口）的批量任务数（batch_mode=true）；批量任务不占用并发额度，可据此了解批量积压情况 |
| tps_limit | number | API 请求速率上限（次/秒）。建议提交间隔 ≥ 1 / tps_limit 秒 |
| tps_burst | integer | 突发容量：瞬间可一次性通过的请求数，用完后按 tps_limit 逐秒恢复 |

---

## 声音事件类别

```
GET /audio-event-classes
```

获取系统支持的全部 521 个声音事件类别列表，含中文名称和分类信息。可用于在提交任务时填写 `audio_events_classes` 字段。

### 响应 200 OK

```json
[
  {
    "index": 0,
    "mid": "/m/09x0r",
    "display_name": "Speech",           // 声音事件英文原名
    "zh_name": "语音",                  // 中文名称
    "category": "Human sounds",         // 英文大类
    "category_zh": "人声与人体声音"      // 中文大类
  },
  {
    "index": 42,
    "mid": "/m/01b_21",
    "display_name": "Cough",
    "zh_name": "咳嗽",
    "category": "Human sounds",
    "category_zh": "人声与人体声音"
  }
  // ... 共 521 项
]
```

> 共 8 个大类：人声与人体声音、动物声音、音乐、自然声音、交通工具、家居与工具、电子设备、环境与背景声。在前端上传页面可通过交互界面按类别选择，无需手动填写类名。

---

## 获取定价

```
GET /pricing
```

获取当前生效的计费单价配置，供客户端实时计算预估费用。**该接口公开，无需认证**。响应带 `Cache-Control: public, max-age=300`，服务端亦有 5 分钟缓存，价格调整后下一次请求即生效。

### 响应 200 OK

```json
{
  "standard_price_per_hour": 1.5,       // 标准档单价（元/小时）
  "zh_lite_price_per_hour": 0.5,        // 优化档单价（元/小时；zh_lite 为历史字段名）
  "diarize_price_per_hour": 0.3,        // 说话人分离附加单价（元/小时）
  "audio_events_price_per_hour": 0.2,   // 声音事件检测附加单价（元/小时）
  "batch_discount": 0.5,                // 批量任务折扣系数（0.5 = 五折）
  "updated_at": "2026-03-09T10:00:00Z"  // 定价最后更新时间
}
```

### 字段说明

| 字段 | 类型 | 说明 |
|------|------|------|
| standard_price_per_hour | float | 标准档单价（元/小时） |
| zh_lite_price_per_hour | float | 优化档单价（元/小时；zh_lite 为历史字段名） |
| diarize_price_per_hour | float | 说话人分离附加单价（元/小时） |
| audio_events_price_per_hour | float | 声音事件检测附加单价（元/小时） |
| batch_discount | float | 批量任务折扣系数（如 0.5 表示五折） |
| updated_at | string | 定价最后更新时间（ISO 8601） |

> 计费最小单位为 1 分钟（向上取整）。单任务费用 = （基础单价 + 各启用附加单价）/ 60 × 计费分钟数；批量任务再乘以 `batch_discount`。每个任务的实际计费明细见 `GET /tasks/:id/result` 返回的 `billing` 段落。

---

## 账单流水

```
GET /billing/transactions
```

获取当前用户的账单流水记录，按时间倒序排列。

### 查询参数

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| page | integer | 可选 | 页码，从 1 开始，默认 1 |
| page_size | integer | 可选 | 每页条数，最大 100，默认 20 |

### 响应 200 OK

```json
{
  "transactions": [
    {
      "id": "a1b2c3d4-...",
      "user_id": "550e8400-...",
      "task_id": "660f9511-...",           // 关联任务 ID（充值/退款时为 null）
      "type": "deduction",                 // deduction（扣费）/ recharge（充值）/ refund（退款）
      "amount": "-1.5000",                 // 金额，扣费为负、充值为正、退款为负
      "balance_before": "100.0000",
      "balance_after": "98.5000",
      "description": "任务扣费 - 标准转写 3.5 分钟",
      "created_at": "2026-03-09T10:05:30Z"
    }
  ],
  "total": 42,
  "page": 1,
  "page_size": 20
}
```

---

## 任务选项（options）完整参考

提交任务时通过 `options` 字段控制处理行为。所有字段均有默认值，可只传需要覆盖的字段。

**依赖规则：**
- `enable_diarize=true` 且 `enable_asr=false`：返回 400（说话人分离依赖转写结果）
- 三项 `enable_*` 全为 false：返回 400

### 功能开关

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| enable_asr | bool | true | 语音转写，含自动语音段切分 |
| enable_diarize | bool | true | 说话人分离，需同时启用 ASR |
| enable_audio_events | bool | true | 声音事件检测，支持 521 类 |
| optimize | bool \| null | null | 转写档位：null=按检测语言自动；true=优化档（更快、更省，段级时间戳，无 words），适用中文及英/西/法/葡，其他语言即使为 true 也自动落标准档；false=标准档（高质量，词/字级时间戳，全语言）。详见"转写模式对比"。 |

### ASR 参数

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| asr_language | string \| null | null | ISO 639-1/3 语言代码，null 自动检测。支持 99 种语言，常用：zh（中文）、en（英语）、ja（日语）、ko（韩语）、fr（法语）、de（德语）、es（西班牙语）、ru（俄语）、ar（阿拉伯语）、pt（葡萄牙语）、th（泰语）、vi（越南语）、hi（印地语） |
| asr_beam_size | integer | 5 | Beam Search 宽度，越大精度越高但速度越慢，推荐 5 |
| asr_temperature | float | 0.0 | 解码温度，0 为贪心解码，>0 启用采样（通常无需修改） |
| asr_best_of | integer \| null | null | Best-of 采样数（1-20），仅在 asr_temperature > 0 时生效；null 表示不启用 |
| hotwords | string[] | null | 热词列表，指定后系统会提升这些词被识别的概率，适合专有名词、品牌名、人名等 |
| initial_prompt | string \| null | null | 初始提示文本，可引导转写风格。填写语气词（"嗯，啊，那个"）可保留口语填充词；填写专有名词可提升识别准确率。null 表示不设置提示 |

### VAD 参数

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| vad_threshold | float | 0.35 | 语音概率阈值（0-1），越高越保守，越低越灵敏。默认 0.35 对背景音乐场景友好 |
| vad_min_speech_ms | integer | 250 | 最短语音段时长（毫秒），短于此的段被忽略 |
| vad_min_silence_ms | integer | 100 | 最短静音间隔（毫秒），短于此的间隙不切断 |
| vad_speech_pad_ms | integer | 30 | 语音段前后填充时长（毫秒） |
| no_speech_threshold | float | 0.6 | 无语音概率阈值，高于此值的片段视为无语音丢弃。调高可减少幻觉，调低可保留低置信度语音 |

### 说话人分离参数

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| num_speakers | integer \| null | null | 固定说话人数量，null 表示自动检测（推荐） |
| min_speakers | integer | 1 | 自动检测时最小说话人数 |
| max_speakers | integer | 10 | 自动检测时最大说话人数 |
| diarize_min_segment_sec | float | 0.5 | 提取声纹向量所需的最短语音段时长（秒），短于此值的段跳过，过短片段向量质量差 |
| diarize_single_speaker_threshold | float | 0.05 | Silhouette Score 阈值，低于此值判定为单说话人 |

### 声音事件参数

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| audio_events_threshold | float | 0.3 | 声音事件置信度阈值，仅返回超过此值的事件 |
| audio_events_classes | string[] \| null | null | 指定监听的声音事件类名列表；null 检测全部 521 类 |

---

## Webhook 回调

任务进入终态（done / failed / abandoned）后，系统会向 `callback_url` 发送一次 HTTP POST 请求，请求体为精简的任务状态通知 JSON，包含 `task_id`、`status`、`created_at`、`finished_at`、`error_message` 五个字段。如需完整任务详情或转写结果，请在收到回调后调用 `GET /tasks/:id` 和 `GET /tasks/:id/result`。

### 触发条件

| 状态 | 说明 |
|------|------|
| done | 任务处理成功完成，结果已存储，可通过 /result 接口获取 |
| failed | 任务因不可恢复的错误直接失败（如文件大小/时长超限），不会重试 |
| abandoned | 任务多次重试后仍然失败，最终放弃 |

> 普通的任务处理失败（将自动重试）**不会**触发回调。仅上述终态会触发。

### 回调请求格式

```
POST https://your-server.com/webhook HTTP/1.1
Content-Type: application/json
X-Signature: sha256=3d9e5f2a1b8c4d7e...  // 仅设置了 callback_secret 时附带

{
  "task_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "done",
  "created_at": "2026-03-09T10:00:00Z",
  "finished_at": "2026-03-09T10:05:30Z",
  "error_message": null                    // 仅失败时包含错误描述
}
```

### 签名验证（X-Signature）

若创建任务时设置了 `callback_secret`，每次回调请求都会附带签名头：

```
X-Signature: sha256=<hex(HMAC-SHA256(secret, body_bytes))>
```

服务端应对请求体重新计算签名，并与请求头比对，以防止伪造回调。示例：

**Python：**

```python
import hmac, hashlib

def verify_callback(secret: str, body: bytes, signature_header: str) -> bool:
    """
    signature_header: 请求头 X-Signature 的值，格式为 "sha256=<hex>"
    """
    expected = "sha256=" + hmac.new(
        secret.encode(),
        body,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

# Flask 示例
from flask import Flask, request, abort
app = Flask(__name__)

@app.route("/webhook", methods=["POST"])
def webhook():
    sig = request.headers.get("X-Signature", "")
    if not verify_callback("your-hmac-secret", request.data, sig):
        abort(403)
    task = request.json
    print(f"Task {task['task_id']} finished: {task['status']}")
    return "", 200
```

**Node.js：**

```javascript
const crypto = require('crypto');

function verifyCallback(secret, bodyBuffer, signatureHeader) {
  // signatureHeader: "sha256=<hex>"
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(bodyBuffer)
    .digest('hex');
  return crypto.timingSafeEqual(
    Buffer.from(expected),
    Buffer.from(signatureHeader)
  );
}

// Express 示例（注意：需要原始 body，不能用 json() 中间件之后的对象）
const express = require('express');
const app = express();

app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
  const sig = req.headers['x-signature'] || '';
  if (!verifyCallback('your-hmac-secret', req.body, sig)) {
    return res.status(403).end();
  }
  const task = JSON.parse(req.body);
  console.log(`Task ${task.task_id} finished: ${task.status}`);
  res.status(200).end();
});
```

**Go：**

```go
package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"fmt"
	"io"
	"net/http"
)

func verifyCallback(secret string, body []byte, signatureHeader string) bool {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write(body)
	expected := "sha256=" + hex.EncodeToString(mac.Sum(nil))
	return hmac.Equal([]byte(expected), []byte(signatureHeader))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	body, _ := io.ReadAll(r.Body)
	sig := r.Header.Get("X-Signature")
	if !verifyCallback("your-hmac-secret", body, sig) {
		http.Error(w, "forbidden", http.StatusForbidden)
		return
	}
	fmt.Printf("Callback received: %s\n", body)
	w.WriteHeader(http.StatusOK)
}
```

### 重试策略

若回调请求失败（网络错误或响应非 2xx），系统会自动重试：

| 次数 | 延迟 | 说明 |
|------|------|------|
| 第 1 次 | 立即发送 | 任务进入终态时立即触发 |
| 第 2 次 | 1 秒后 | 首次失败后等待 1 秒重试 |
| 第 3 次 | 5 秒后 | 二次失败后等待 5 秒重试 |
| 第 4 次 | 30 秒后 | 三次失败后等待 30 秒最后一次重试，之后放弃 |

> 你的回调服务器应尽快返回 `2xx`，避免处理逻辑阻塞响应。建议先将请求入队，异步处理任务结果。

---

## 请求限速

> **请求限速对全部 API 接口全局生效**——不仅是提交任务，**任意 `/v1` 请求**（查询任务、获取结果、查账户、列出任务等）都计入**同一个**限速额度。它衡量的是"每秒发起多少个请求"，与请求的是哪个接口无关。

### 原理：令牌桶

每个账户维护一个令牌桶：桶的容量 = **突发容量**（`tps_burst`），并按**速率**（`tps_limit`，次/秒）持续补充令牌。**每发起一个 API 请求消耗一个令牌**，桶空时请求被拒绝。因此：

- 桶通常是满的，你可以在**瞬间连续发起最多「突发容量」个请求**；
- 之后须按**每秒「速率」个**的节奏发送，令牌用完即触发限速；
- 稳态下建议的**最小请求间隔 ≈ 1 / `tps_limit` 秒**。

### 限速数值以后台 / 接口为准

不同账户等级有不同的默认速率与突发容量，且账户可被单独调整、购买带加成的资源包也会提升——**具体数值会随运营策略变化，本文档不列固定值**。请以下列权威来源为准（二者一致，实时反映当前生效值）：

- **`GET /account`** 返回的 `tps_limit` / `tps_burst`——程序化读取，据此自适应控制提交节奏；
- 账号后台「**等级信息**」页展示的当前额度。

### 触发后果

超出限速返回 **429**（`error_code=rate_limited`），响应带 `Retry-After` 头，请按提示秒数退避重试；建议客户端实现指数退避。

### 与并发任务数限制的区别

请求限速之外，另有一个**独立**的维度——**并发任务数**：限制同时处于处理中的任务数量，**仅在提交任务（`POST /tasks`、`POST /tasks/batch`）时检查**，超出返回 429（`error_code=concurrency_limit_exceeded`）；批量任务不受此约束。二者互不影响：

| 维度 | 作用范围 | 触发码 |
|------|----------|--------|
| **请求限速** | 全部 /v1 请求（按每秒次数） | `rate_limited` |
| **并发任务数** | 仅提交任务（按处理中任务数） | `concurrency_limit_exceeded` |

并发上限同样按账户等级设定、可被资源包加成，**具体数值以 `GET /account` 的 `concurrency_limit` / `available_concurrency` 或账号后台「等级信息」页为准**（批量任务豁免并发限制）。

---

## 完整调用示例

以下示例展示两种典型调用模式（使用 curl）：**轮询**适合脚本和调试，**Webhook 回调**适合生产系统。

```bash
# ═══ 方式一：轮询（Polling）═══════════════════════════════════════

# 1. 提交任务（包含完整选项）
TASK_ID=$(curl -s -X POST https://api.auralwise.cn/v1/tasks \
  -H "X-API-Key: asr_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "audio_url": "https://example.com/meeting.mp3",
    "audio_filename": "meeting.mp3",
    "options": {
      "enable_asr": true,
      "enable_diarize": true,
      "enable_audio_events": false,
      "optimize": true,
      "asr_language": "zh",
      "asr_beam_size": 5,
      "vad_threshold": 0.35,
      "num_speakers": null,
      "max_speakers": 5
    }
  }' | jq -r .id)

echo "Task ID: $TASK_ID"

# 2. 轮询等待完成（建议间隔 5 秒）
while true; do
  RESP=$(curl -s -H "X-API-Key: asr_xxxx" https://api.auralwise.cn/v1/tasks/$TASK_ID)
  STATUS=$(echo $RESP | jq -r .status)
  echo "Status: $STATUS"
  [ "$STATUS" = "done" ]      && break
  [ "$STATUS" = "abandoned" ] && echo "Task abandoned!" && exit 1
  sleep 5
done

# 3. 获取转写结果
curl -s -H "X-API-Key: asr_xxxx" \
  https://api.auralwise.cn/v1/tasks/$TASK_ID/result | jq .


# ═══ 方式二：Webhook 回调（推荐用于生产）══════════════════════════

# 提交时指定 callback_url，任务完成后系统主动 POST 通知
curl -s -X POST https://api.auralwise.cn/v1/tasks \
  -H "X-API-Key: asr_xxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "audio_url": "https://example.com/meeting.mp3",
    "options": {
      "enable_asr": true,
      "enable_diarize": true,
      "asr_language": "zh"
    },
    "callback_url": "https://your-server.com/webhook",
    "callback_secret": "your-secret-key"
  }' | jq .

# 你的服务器收到的回调请求（示例）：
# POST https://your-server.com/webhook
# Content-Type: application/json
# X-Signature: sha256=3d9e5f2a...
#
# { "task_id": "...", "status": "done", "created_at": "...", "finished_at": "...", "error_message": null }
#
# 验证签名后，调用 GET /tasks/:id 和 GET /tasks/:id/result 获取完整详情和转写结果
```

---

## 错误处理

所有错误响应均为 JSON 格式，包含 `error` 字段描述错误原因：

```json
{
  "error": "错误描述"
}
```

| HTTP 状态码 | 说明 |
|-------------|------|
| 400 | 请求格式错误或参数不合法（如 options 依赖规则冲突） |
| 401 | API Key 缺失或无效 |
| 402 | 可用余额不足（含被处理中任务预占的金额），请充值或等待任务完成后重试（error_code=insufficient_balance） |
| 403 | 权限不足（如访问其他用户的任务） |
| 404 | 任务不存在，或任务尚未完成（查询结果时） |
| 409 | 操作冲突（如删除 processing 状态的任务） |
| 429 | 触发限制：并发任务数达上限（error_code=concurrency_limit_exceeded，等待现有任务完成后重试）；或 API 请求速率超限（error_code=rate_limited，响应带 Retry-After 头，按提示秒数退避重试） |
| 500 | 服务内部错误 |
