# 音频 (https://hypit.ai/zh/api-reference/audio/)

> 语音合成与转写是同步的；音乐生成是一个 Job。

音频接口的响应形态如下：

| 路由                                         | 形态                                         |
| ------------------------------------------ | ------------------------------------------ |
| `POST /v1/audio/speech`                    | **同步**——返回音频字节，或 JSON                      |
| `POST /v1/audio/transcriptions`            | **同步**——返回文本或 JSON                         |
| `GET /v1/audio/transcriptions/:request_id` | **只读**——取回已保存的转写结果                         |
| `POST /v1/audio/music`                     | **异步**——返回一个 [Job](/zh/api-reference/jobs) |

<Callout type="info" title="先确认可用模型">
  音频模型按部署逐个启用。写死模型名之前，先查一下这把 Key 能路由到哪些：

  ```bash
  curl -s https://hypit.ai/v1/models \
    -H "Authorization: Bearer $HYPIT_API_KEY" |
    jq -r '.data[] | select(.endpoints[] | test("audio|transcriptions")) | "\(.id)\t\(.endpoints)"'
  ```
</Callout>

## 语音合成 [#语音合成]

```bash
curl https://hypit.ai/v1/audio/speech \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "'"$MODEL"'", "input": "那只船翻过了堰。", "voice": "alloy"}' \
  --output speech.mp3
```

OpenAI SDK 的 `client.audio.speech.*` 可以直接打到这条路由。

### 请求体 [#请求体]

| 字段                                  | 类型        | 说明                                                         |
| ----------------------------------- | --------- | ---------------------------------------------------------- |
| `model`                             | string    | **必填**                                                     |
| `input`                             | string    | 文本内容。`text` 是同义字段                                          |
| `prompt`                            | string    | 自由描述，支持的模型上生效                                              |
| `lyrics`                            | string    | 歌词                                                         |
| `voice`                             | string    | 具名音色                                                       |
| `reference_id`                      | string    | 克隆音色 id                                                    |
| `voice_description`                 | string    | 用描述代替具名音色                                                  |
| `response_format`                   | string    | `mp3`、`wav`、`pcm`、`opus`、`flac`、`aac`、`ogg`。`format` 是同义字段 |
| `speed`、`volume`、`loudness`         | number    | 取值范围由厂商定义                                                  |
| `sample_rate`、`bitrate`             | integer   | `mp3_bitrate` 是 `bitrate` 的同义字段                            |
| `duration_seconds`                  | number    | `seconds` 和 `duration` 是同义字段                               |
| `n`                                 | integer   | 1–8，默认 `1`                                                 |
| `seed`                              | integer   |                                                            |
| `language`                          | string    |                                                            |
| `instrumental`、`loop`               | boolean   |                                                            |
| `guidance_scale`、`prompt_influence` | number    |                                                            |
| `reference_audio`                   | string 数组 | `http(s)` 或 `data:` URL                                    |
| `auto_generate_text`                | boolean   |                                                            |
| `output`                            | string    | `binary`（默认）、`b64_json` 或 `url`                            |

`input`/`text`、`prompt`、`lyrics`、`voice_description` 至少要有一个。`input` 与 `lyrics` 合计上限
**4 万字符**；整个 JSON 请求体上限 **12 MiB**。未建模的字段原样转发给上游。

### MiMo VoiceClone [#mimo-voiceclone]

`mimo-v2.5-tts-voiceclone` 需要且只接受一个 MP3/WAV 声音样本。推荐放进
`reference_audio`；这里既可以给 `http(s)` URL，也可以给完整的 data URL，网关会按渠道需要抓取并
内联。直接使用 `voice` 时必须传 data URL（也兼容旧客户端传裸 base64）：

```json
{
  "model": "mimo-v2.5-tts-voiceclone",
  "input": "这段话会使用参考音频中的声音来朗读。",
  "reference_audio": ["data:audio/wav;base64,UklGRg..."],
  "response_format": "wav"
}
```

MiMo 要求 data URL 使用 `data:audio/mpeg;base64,...` 或 `data:audio/wav;base64,...`，base64
部分最大 10 MiB。网关会按文件魔数校验，伪装 MIME 或其他音频容器会在请求上游之前返回 400。

### 响应 [#响应]

`output` 未设置或为 `binary` 时，响应是**原始音频字节**，`Content-Type` 由嗅探得出（默认
`audio/mpeg`），并带 `X-Content-Type-Options: nosniff` 和
`Content-Disposition: inline; filename="job_….mp3"`。

`output` 为 `b64_json` 或 `url` 时，响应是 JSON：

```json
{
  "object": "audio.speech",
  "created": 1787824589,
  "model": "…",
  "format": "mp3",
  "mime_type": "audio/mpeg",
  "b64_json": "SUQzBA…",
  "seconds": 3.4,
  "usage": { "characters": 28, "audio_seconds": 3.4 }
}
```

注意 `output: "url"` 在无法产生存储 URL 时会静默退回 `b64_json`，所以两个字段都要处理。音色设计类
请求返回 `"object": "audio.voice_previews"` 和一个 `previews[]` 数组，每项包含
`{voice_id, name, description, b64_json|url, mime_type, format, seconds}`。

## 转写 [#转写]

```bash
curl https://hypit.ai/v1/audio/transcriptions \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -F model="$MODEL" \
  -F file=@interview.mp3 \
  -F response_format=verbose_json
```

OpenAI SDK 的 `client.audio.transcriptions.*` 可以直接打到这条路由。

接受 `multipart/form-data` **或** `application/json`，其余 Content-Type 返回
`400 unsupported_content_type`。

| 字段                          | 说明                                           |
| --------------------------- | -------------------------------------------- |
| `model`                     | **必填**                                       |
| `file`                      | 音频文件——仅 multipart                            |
| `url`                       | 用 `http(s)` URL 代替文件。`file` 与 `url` 必须且只能有一个 |
| `response_format`           | `json`（默认）、`text`、`verbose_json`             |
| `language`                  | ISO 语言码提示                                    |
| `prompt`                    | 上下文提示                                        |
| `temperature`               | number                                       |
| `timestamp_granularities[]` | 可重复：`segment` 和/或 `word`。不带方括号的写法也接受         |

限制：单个上传文件 **100 MiB**，整个 multipart 请求体 **120 MiB**，JSON 请求体 **4 MiB**。分片声明
的 `Content-Type` 会被忽略——我们嗅探字节，接受的容器为 WAV、FLAC、Ogg（Vorbis/Opus）、MP3、
ADTS AAC、M4A、MP4 和 WebM。&#x2A;*无头的裸 PCM 会被拒绝。**

`response_format: "text"` 返回 `text/plain`，正文就是转写结果本身。其余取值返回 JSON：

```json
{
  "task": "transcribe",
  "language": "en",
  "duration": 92.4,
  "text": "…",
  "segments": [
    { "id": 0, "start": 0.0, "end": 3.2, "text": "…", "no_speech_prob": 0.01 }
  ],
  "words": [{ "word": "the", "start": 0.10, "end": 0.22 }],
  "usage": { "audio_seconds": 92.4 }
}
```

`task`、`language`、`duration`、`segments`、`words` 只在 `verbose_json` 下出现。`usage` 是我们加的，
在普通 `json` 形态下也有——OpenAI 那边是没有的。

### 取回已保存的转写结果 [#取回已保存的转写结果]

保存转写 POST 响应头中服务端生成的 `X-Request-Id` 和 `Location`。持久化预留成功后，
`Location` 会指向以下只读接口；POST 随后因上游结果不明而报错时也可使用这个链接：

```bash
curl https://hypit.ai/v1/audio/transcriptions/req_YOUR_SERVER_REQUEST_ID \
  -H "Authorization: Bearer $HYPIT_API_KEY"
```

使用原账号下当前有效的 API Key。OAuth 需要 `user:jobs`，只有 `user:inference:audio` 不够。
模型后来下架、Key 更换分组都不影响本账号历史结果；其他账号与不存在的 ID 一律返回相同的 404。
GET 不重新提交上游，也不重复扣费。

```json
{
  "id": "req_YOUR_SERVER_REQUEST_ID",
  "object": "audio.transcription",
  "model": "your-model",
  "status": "completed",
  "settlement_state": "settled",
  "created_at": 1788900000,
  "completed_at": 1788900010,
  "expires_at": 1791492010,
  "result": {
    "text": "你好，世界。",
    "language": "zh",
    "duration": 4.5,
    "usage": { "audio_seconds": 4.5 }
  }
}
```

无论 POST 的 `response_format` 是什么，取回的 `result` 都是 JSON；上游提供时还包含
`segments`、`words`。上游原始响应和输入 URL 不会暴露。

| HTTP | `status`               | 含义                                 |
| ---- | ---------------------- | ---------------------------------- |
| 202  | `pending`、`processing` | 尚无可读结果；至少等待 `Retry-After` 指定的秒数再查询 |
| 200  | `completed`            | 正文位于 `result`                      |
| 200  | `failed`               | 上游操作已明确失败                          |
| 200  | `unavailable`          | 处理已结束，但没有可靠的已保存正文                  |
| 410  | `expired`              | 正文保留期已结束                           |

`status` 表示正文状态，`settlement_state` 独立表示结算状态。历史账单仍为 `unknown` 时，
已恢复正文可以是 `completed`。正常成功的 POST 在发送正文前保存结果；已受理的 Replicate
转写任务还可凭持久化的上游任务 ID 在进程重启后继续查询。并非所有供应商、所有错误都具备后台结果恢复能力。

正文沿用部署的存储保留期：默认 30 天，从首次保存正文算起；负数配置表示永久保留。
到期删除正文，但保留财务记录。响应标记 `Cache-Control: private, no-store`。
保存后的公开结果沿用上游缓冲响应的 32 MiB 上限，超限明确拒绝，不静默截断。
已确认超限且无法交付时返回 502，退还客户预留，正文标记 `unavailable`，客户结算为零。
普通客户端断线或 GET 缺正文不能作为退款依据。

此接口要求**服务端生成的**请求 ID。若断线发生在响应头送达之前，客户端无法通过此接口发现该 ID。
客户端传入的 `X-Request-Id` 不是幂等键；再次 POST 会创建新请求，并可能再次收费。
这次结果取回扩展不提供 POST 幂等保证。

## 音乐 [#音乐]

`POST /v1/audio/music` 是异步的。它接受与 `/v1/audio/speech` **完全相同**的请求体——同样的字段、
同样的同义写法、同样的校验、同样的 12 MiB 上限——返回 `202 Accepted` 和一个 `kind` 为 `audio` 的 Job。

```bash
curl https://hypit.ai/v1/audio/music \
  -H "Authorization: Bearer $HYPIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "'"$MODEL"'",
    "prompt": "慢速 lo-fi 钢琴，窗外下雨",
    "lyrics": "",
    "instrumental": true,
    "duration_seconds": 60
  }'
```

这里真正起作用的字段是 `prompt`、`lyrics`、`instrumental`、`duration_seconds`、`voice`、
`reference_id`、`reference_audio`、`language`、`seed` 和 `n`。`output` 会被解析和校验，但没有任何
效果：Job 的产物一律通过 assets 接口交付。

之后的轮询与取产物流程和[异步任务](/zh/api-reference/jobs)完全一样。另有两个别名指向同一个 handler：

```bash
curl https://hypit.ai/v1/audio/music/$JOB_ID \
  -H "Authorization: Bearer $HYPIT_API_KEY"

curl -L -o track.mp3 https://hypit.ai/v1/audio/music/$JOB_ID/content \
  -H "Authorization: Bearer $HYPIT_API_KEY"
```

## SDK 示例 [#sdk-示例]

```python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["HYPIT_API_KEY"],
    base_url=os.environ.get("HYPIT_BASE_URL", "https://hypit.ai/v1"),
)

with client.audio.speech.with_streaming_response.create(
    model=os.environ["MODEL"],
    voice="alloy",
    input="那只船翻过了堰。",
) as response:
    response.stream_to_file("speech.mp3")

with open("interview.mp3", "rb") as f:
    print(client.audio.transcriptions.create(model=os.environ["MODEL"], file=f).text)
```

```js
import { createWriteStream } from "node:fs";
import { Readable } from "node:stream";
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.HYPIT_API_KEY,
  baseURL: process.env.HYPIT_BASE_URL ?? "https://hypit.ai/v1",
});

const speech = await client.audio.speech.create({
  model: process.env.MODEL,
  voice: "alloy",
  input: "那只船翻过了堰。",
});
Readable.fromWeb(speech.body).pipe(createWriteStream("speech.mp3"));
```

## 这三条路由特有的错误 [#这三条路由特有的错误]

| 状态    | `code`                                                  | 路由    | 含义                                   |
| ----- | ------------------------------------------------------- | ----- | ------------------------------------ |
| `400` | `missing_model`                                         | 全部    | `model` 必填                           |
| `400` | `missing_input`                                         | 合成、音乐 | 没有文本、prompt、歌词或音色描述                  |
| `413` | `text_too_long`                                         | 合成、音乐 | 超过 4 万字符                             |
| `400` | `invalid_n`                                             | 合成、音乐 | `n` 必须在 1–8 之间                       |
| `400` | `invalid_duration`                                      | 合成、音乐 | 时长不能为负                               |
| `400` | `invalid_response_format`                               | 合成、音乐 | 不在七种音频格式之内                           |
| `400` | `invalid_output`                                        | 合成、音乐 | 不是 `binary`、`b64_json` 或 `url`       |
| `413` | `voice_sample_too_large`                                | 合成    | MiMo VoiceClone 的 base64 样本超过 10 MiB |
| `400` | `unsupported_content_type`                              | 转写    | 既不是 JSON 也不是 multipart               |
| `400` | `missing_file` / `ambiguous_input`                      | 转写    | `file` 与 `url` 必须且只能有一个              |
| `400` | `invalid_response_format`                               | 转写    | 不是 `json`、`text` 或 `verbose_json`    |
| `400` | `invalid_timestamp_granularity`                         | 转写    | 不是 `segment` 或 `word`                |
| `400` | `invalid_temperature`                                   | 转写    | 不是数字                                 |
| `400` | `unsupported_audio_format`                              | 转写    | 字节内容不是可识别的容器                         |
| `400` | `empty_file` / `unreadable_file`                        | 转写    | 分片里没有可用内容                            |
| `413` | `file_too_large`                                        | 转写    | 超过 100 MiB                           |
| `413` | `body_too_large`                                        | 全部    | 超过该路由的请求体上限                          |
| `502` | `empty_audio_response` / `empty_transcription_response` | 合成、转写 | 上游没有返回可用内容                           |