# 声音转文字字幕 API 接入教程

> 在线效果体验：https://wenzi.ftcxx.com/  
> 接口地址：`https://api.hihookeji.com/api/mediatotext/index`  
> 返回格式：`application/json`  
> 请求方式：`HTTP POST`

---

## 一、接口概述

本接口将音频转为文本或字幕（SRT / VTT / ASS / TSV 等），支持异步回调与同步返回；也可做字幕校对、声音校对等。

| 项目 | 说明 |
|------|------|
| 接口地址 | `https://api.hihookeji.com/api/mediatotext/index` |
| 认证方式 | 请求体字段 `key`（你的 API 密钥，与业务参数一并提交） |
| 请求示例 | `https://api.hihookeji.com/api/mediatotext/index` |
| Content-Type | `application/json`（推荐）或 `application/x-www-form-urlencoded` |
| 业务判断 | 统一用响应字段 **`code`**：`200` 成功，失败常见为 `500` |
| 结果获取 | `type=1` 异步回调；`type=2` 同步返回（音频时长需 **小于 10 分钟**） |

---

## 二、请求参数

| 参数名 | 类型 | 必填 | 描述 | 示例 |
|--------|------|------|------|------|
| key | string | 是 | API 密钥，放在请求体中与其它参数一起提交 | `你的密钥` |
| audio_url | string | 是 | 音频链接（公网可访问）；`type=2` 时时长须 **小于 10 分钟**；建议标准 **mp3 / wav**，URL 以 `.mp3` 或 `.wav` 结尾 | `https://a.b.com/2116.mp3` |
| notify_url | string | 是 | 任务结果通知地址（需公网可达；同步模式也建议传） | 见下方「回调测试」 |
| stems | number | 是 | 转换准确率：`1` 极速模式（快）；`2` 更高准确率（慢） | `2` |
| mode | number | 是 | 音频转换类型，见下方说明 | `1` |
| lang | string | 是 | 音频语言：`zh` 中文、`yue` 粤语、`en` 英语、`ja` 日语、`ko` 韩语、`other` 其他 | `zh` |
| type | number | 否 | `1` 异步（默认）；`2` 同步（音频 &lt; 10 分钟） | `1` |
| source_text | string | 否 | 对轴、文案校对等场景需填写原文案 | — |

### 2.1 mode 取值

| mode | 说明 |
|------|------|
| 1 | 文本模式 |
| 2 | SRT 格式 |
| 3 | VTT 格式 |
| 4 | ASS 格式 |
| 5 | TSV 格式 |
| 6 | 字幕校对模式（配合 `source_text`） |
| 7 | 声音校对 |

**回调测试**：联调异步回调时，建议先打开 [https://webhook.site/](https://webhook.site/) 获取自己的唯一 URL，将该地址填入 `notify_url`。提交任务后可在网页上直接查看回调字段与提取结果，无需先部署自己的回调服务。

---

## 三、接口更新

### 2025-10-23

1. 新增同步返回：用 `type` 区分；`type=2` 同步模式下，音频时长需 **小于 10 分钟**。

---

## 四、返回结果说明

### 4.1 任务提交成功（异步 type=1）

立即返回任务 ID，处理完成后通过 `notify_url` 回调。请自行保存 `data.taskid`，回调会带同一任务 ID。

| 字段 | 说明 |
|------|------|
| code | `200` 表示任务正确提交 |
| msg | 说明信息，如 `ok` |
| data.taskid | 任务 ID |

```json
{
  "code": 200,
  "data": {
    "taskid": "172733860562215209765004"
  },
  "msg": "ok"
}
```

### 4.2 任务提交失败

```json
{
  "code": 500,
  "msg": "提取的类型：Vocals,不能为空"
}
```

### 4.3 同步返回（type=2）

音频时长须小于 10 分钟；成功时 `code=200`，提取结果一般在响应体中（字段以实际返回为准，常见含 `text` 等）。超时或长音频请改用 `type=1` 异步。

### 4.4 异步回调 Notify（type=1）

服务端处理完成后，向 `notify_url` **POST** 推送（表单字段；**不是** JSON Body）。

| 字段 | 说明 |
|------|------|
| errcode | **`0` 成功**；**`1001` 失败**（与提交响应的 `code: 200` 不同，回调按此处判断） |
| taskid | 任务 ID，与提交返回一致 |
| text | 提取结果（文本或字幕内容） |
| otherdata | 其它参数，如文案校对结果等 |

回调数据示意：

```json
{
  "errcode": 0,
  "taskid": "172733860562215209765004",
  "text": "1111",
  "otherdata": {
    "state": "success",
    "score": 90
  }
}
```

> 实际推送多为表单字段；`otherdata` 可能是 JSON 字符串，接收后需再 `json.loads` / `json_decode`。

---

## 五、接入流程建议

1. 准备公网可访问的音频 URL（建议 mp3/wav，后缀正确）。
2. 申请 API Key，作为请求体字段 `key` 与其它参数一起提交。
3. 选择 `stems`、`mode`、`lang`；校对/对轴时填 `source_text`。
4. 时长较短且需立即拿结果：可用 `type=2`（须 &lt; 10 分钟）；否则用 `type=1`，`notify_url` 建议用 [webhook.site](https://webhook.site/) 联调。
5. 异步成功后保存 `data.taskid`，在回调中用同一 `taskid` 关联 `text` / `otherdata`。

---

## 六、Python 完整代码

依赖：`requests`（`pip install requests`）

```python
# -*- coding: utf-8 -*-
"""
声音转文字字幕 API - Python 完整示例
接口: https://api.hihookeji.com/api/mediatotext/index
"""

import json
import os

import requests

API_URL = "https://api.hihookeji.com/api/mediatotext/index"
API_KEY = "你的密钥"  # 替换为真实密钥


def media_to_text(
    audio_url,
    notify_url,
    stems=2,
    mode=1,
    lang="zh",
    result_type=1,
    source_text=None,
    timeout=120,
):
    """
    声音转文字 / 字幕。

    :param audio_url: 音频公网 URL
    :param notify_url: 异步回调地址
    :param stems: 1=极速，2=更高准确率
    :param mode: 1文本 2SRT 3VTT 4ASS 5TSV 6字幕校对 7声音校对
    :param lang: zh/yue/en/ja/ko/other
    :param result_type: 1=异步，2=同步（音频<10分钟）
    :param source_text: 对轴/校对文案（可选）
    """
    payload = {
        "key": API_KEY,
        "audio_url": audio_url,
        "notify_url": notify_url,
        "stems": stems,
        "mode": mode,
        "lang": lang,
        "type": result_type,
    }
    if source_text:
        payload["source_text"] = source_text

    resp = requests.post(
        API_URL,
        json=payload,
        headers={"Content-Type": "application/json"},
        timeout=timeout,
    )
    resp.raise_for_status()
    return resp.json()


# ---------------------------------------------------------------------------
# Flask 异步回调接收示例（可选）
# 安装: pip install flask
# ---------------------------------------------------------------------------
def create_notify_app(save_dir="./callback_text"):
    from flask import Flask, request, jsonify

    app = Flask(__name__)
    os.makedirs(save_dir, exist_ok=True)

    @app.route("/Notify", methods=["POST"])
    def notify():
        taskid = request.form.get("taskid")
        errcode = request.form.get("errcode")  # 成功 0，失败 1001
        text = request.form.get("text") or ""
        otherdata = request.form.get("otherdata")

        if otherdata:
            try:
                otherdata = json.loads(otherdata)
            except Exception:
                pass

        if str(errcode) == "0":
            path = os.path.join(save_dir, f"{taskid}.txt")
            with open(path, "w", encoding="utf-8") as f:
                f.write(text)
            return jsonify({
                "ok": True,
                "taskid": taskid,
                "saved": path,
                "otherdata": otherdata,
                "errcode": errcode,
            })

        return jsonify({
            "ok": False,
            "taskid": taskid,
            "errcode": errcode,
            "text": text,
        })

    return app


if __name__ == "__main__":
    # ---------- 示例 1：异步转文本（type=1）----------
    async_result = media_to_text(
        audio_url="https://a.b.com/2116.mp3",
        notify_url="https://webhook.site/你的唯一ID",
        stems=2,
        mode=1,
        lang="zh",
        result_type=1,
    )
    print("异步提交:", json.dumps(async_result, ensure_ascii=False, indent=2))
    # 成功时 code==200，记录 data.taskid；回调可在 webhook.site 查看

    # ---------- 示例 2：同步（type=2，音频须 < 10 分钟）----------
    sync_result = media_to_text(
        audio_url="https://a.b.com/2116.mp3",
        notify_url="https://webhook.site/你的唯一ID",
        stems=1,
        mode=2,
        lang="zh",
        result_type=2,
        timeout=180,
    )
    print("同步响应:", json.dumps(sync_result, ensure_ascii=False, indent=2))

    # ---------- 启动回调服务（需要时取消注释）----------
    # app = create_notify_app()
    # app.run(host="0.0.0.0", port=8080)
```

---

## 七、PHP 完整代码

需开启 `curl` 扩展。

```php
<?php
/**
 * 声音转文字字幕 API - PHP 完整示例
 * 接口: https://api.hihookeji.com/api/mediatotext/index
 */

define('API_URL', 'https://api.hihookeji.com/api/mediatotext/index');
define('API_KEY', '你的密钥'); // 替换为真实密钥

/**
 * 声音转文字 / 字幕
 *
 * @param array $params
 * @param int   $timeout
 * @return array
 * @throws Exception
 */
function mediaToText(array $params, $timeout = 120)
{
    $payload = [
        'key'        => API_KEY,
        'audio_url'  => $params['audio_url'],
        'notify_url' => $params['notify_url'],
        'stems'      => isset($params['stems']) ? (int)$params['stems'] : 2,
        'mode'       => isset($params['mode']) ? (int)$params['mode'] : 1,
        'lang'       => isset($params['lang']) ? $params['lang'] : 'zh',
        'type'       => isset($params['type']) ? (int)$params['type'] : 1,
    ];
    if (!empty($params['source_text'])) {
        $payload['source_text'] = $params['source_text'];
    }

    $ch = curl_init(API_URL);
    curl_setopt_array($ch, [
        CURLOPT_POST           => true,
        CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
        CURLOPT_POSTFIELDS     => json_encode($payload, JSON_UNESCAPED_UNICODE),
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => $timeout,
        CURLOPT_SSL_VERIFYPEER => true,
    ]);

    $body = curl_exec($ch);
    if ($body === false) {
        $err = curl_error($ch);
        curl_close($ch);
        throw new Exception('请求失败: ' . $err);
    }
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    $result = json_decode($body, true);
    if (!is_array($result)) {
        throw new Exception("响应非 JSON，HTTP={$httpCode}, body={$body}");
    }
    return $result;
}

try {
    // 异步
    $asyncResult = mediaToText([
        'audio_url'  => 'https://a.b.com/2116.mp3',
        'notify_url' => 'https://webhook.site/你的唯一ID',
        'stems'      => 2,
        'mode'       => 1,
        'lang'       => 'zh',
        'type'       => 1,
    ]);
    echo "异步提交:\n" . json_encode($asyncResult, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";

    // 同步（音频须 < 10 分钟）
    $syncResult = mediaToText([
        'audio_url'  => 'https://a.b.com/2116.mp3',
        'notify_url' => 'https://webhook.site/你的唯一ID',
        'stems'      => 1,
        'mode'       => 2,
        'lang'       => 'zh',
        'type'       => 2,
    ], 180);
    echo "同步响应:\n" . json_encode($syncResult, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
} catch (Exception $e) {
    echo '错误: ' . $e->getMessage() . "\n";
}

/*
======================== notify.php 异步回调接收 ========================
<?php
header('Content-Type: application/json; charset=utf-8');

$saveDir = __DIR__ . '/callback_text';
if (!is_dir($saveDir)) {
    mkdir($saveDir, 0755, true);
}

$taskid    = isset($_POST['taskid']) ? $_POST['taskid'] : null;
$errcode   = isset($_POST['errcode']) ? $_POST['errcode'] : null;
$text      = isset($_POST['text']) ? $_POST['text'] : '';
$otherdata = isset($_POST['otherdata']) ? $_POST['otherdata'] : null;
if (is_string($otherdata)) {
    $decoded = json_decode($otherdata, true);
    if (is_array($decoded)) {
        $otherdata = $decoded;
    }
}

if ((string)$errcode === '0') {
    $path = $saveDir . '/' . ($taskid ?: uniqid('task_', true)) . '.txt';
    file_put_contents($path, $text);
    echo json_encode([
        'ok'        => true,
        'taskid'    => $taskid,
        'saved'     => $path,
        'otherdata' => $otherdata,
        'errcode'   => $errcode,
    ], JSON_UNESCAPED_UNICODE);
    exit;
}

echo json_encode([
    'ok'      => false,
    'taskid'  => $taskid,
    'errcode' => $errcode,
    'text'    => $text,
], JSON_UNESCAPED_UNICODE);
*/
```

---

## 八、Java 完整代码

依赖：JDK 8+，使用内置 `HttpURLConnection`，无需第三方库。

```java
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;

/**
 * 声音转文字字幕 API - Java 完整示例
 * 接口: https://api.hihookeji.com/api/mediatotext/index
 */
public class MediaToTextDemo {

    private static final String API_URL = "https://api.hihookeji.com/api/mediatotext/index";
    private static final String API_KEY = "你的密钥"; // 替换为真实密钥

    public static class MediaRequest {
        public String audioUrl;
        public String notifyUrl;
        public int stems = 2;       // 1 极速，2 更高准确率
        public int mode = 1;        // 1文本 2SRT 3VTT 4ASS 5TSV 6字幕校对 7声音校对
        public String lang = "zh";
        public int type = 1;        // 1 异步，2 同步
        public String sourceText;   // 可选
    }

    public static String mediaToText(MediaRequest req, int timeoutMs) throws IOException {
        URL url = new URL(API_URL);
        HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        conn.setRequestMethod("POST");
        conn.setConnectTimeout(15000);
        conn.setReadTimeout(timeoutMs);
        conn.setDoOutput(true);
        conn.setRequestProperty("Content-Type", "application/json; charset=UTF-8");
        conn.setRequestProperty("Accept", "application/json");

        String json = toJson(req);
        byte[] body = json.getBytes(StandardCharsets.UTF_8);
        conn.setRequestProperty("Content-Length", String.valueOf(body.length));
        try (OutputStream os = conn.getOutputStream()) {
            os.write(body);
        }

        int httpCode = conn.getResponseCode();
        InputStream is = (httpCode >= 200 && httpCode < 300)
                ? conn.getInputStream()
                : conn.getErrorStream();
        String resp = readFully(is);
        conn.disconnect();
        return resp;
    }

    private static String toJson(MediaRequest r) {
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append("\"key\":").append(quote(API_KEY)).append(",");
        sb.append("\"audio_url\":").append(quote(r.audioUrl)).append(",");
        sb.append("\"notify_url\":").append(quote(r.notifyUrl)).append(",");
        sb.append("\"stems\":").append(r.stems).append(",");
        sb.append("\"mode\":").append(r.mode).append(",");
        sb.append("\"lang\":").append(quote(r.lang)).append(",");
        sb.append("\"type\":").append(r.type);
        if (r.sourceText != null && !r.sourceText.isEmpty()) {
            sb.append(",\"source_text\":").append(quote(r.sourceText));
        }
        sb.append("}");
        return sb.toString();
    }

    private static String quote(String s) {
        if (s == null) {
            return "null";
        }
        String escaped = s
                .replace("\\", "\\\\")
                .replace("\"", "\\\"")
                .replace("\n", "\\n")
                .replace("\r", "\\r")
                .replace("\t", "\\t");
        return "\"" + escaped + "\"";
    }

    private static String readFully(InputStream is) throws IOException {
        if (is == null) {
            return "";
        }
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        byte[] buf = new byte[4096];
        int n;
        while ((n = is.read(buf)) != -1) {
            bos.write(buf, 0, n);
        }
        return new String(bos.toByteArray(), StandardCharsets.UTF_8);
    }

    /*
     * 回调 Servlet 伪代码：
     * 表单字段: errcode(成功0/失败1001), taskid, text, otherdata
     */

    public static void main(String[] args) throws Exception {
        MediaRequest asyncReq = new MediaRequest();
        asyncReq.audioUrl = "https://a.b.com/2116.mp3";
        asyncReq.notifyUrl = "https://webhook.site/你的唯一ID";
        asyncReq.stems = 2;
        asyncReq.mode = 1;
        asyncReq.lang = "zh";
        asyncReq.type = 1;
        System.out.println("异步提交: " + mediaToText(asyncReq, 60000));

        MediaRequest syncReq = new MediaRequest();
        syncReq.audioUrl = "https://a.b.com/2116.mp3";
        syncReq.notifyUrl = "https://webhook.site/你的唯一ID";
        syncReq.stems = 1;
        syncReq.mode = 2;
        syncReq.lang = "zh";
        syncReq.type = 2;
        System.out.println("同步响应: " + mediaToText(syncReq, 180000));
    }
}
```

---

## 九、常见问题

| 问题 | 处理建议 |
|------|----------|
| 音频链接失败 | 须公网可访问；建议标准 **mp3 / wav**，URL 以 **`.mp3` / `.wav` 结尾**；OSS 鉴权长链无后缀时易失败，可 ffmpeg 转码后转存再提交 |
| 同步无结果 / 超时 | `type=2` 仅支持时长 **&lt; 10 分钟**；更长音频用 `type=1` 异步 |
| mode / stems 含义搞混 | `stems` 控制速度与准确率；`mode` 控制输出形态（文本/字幕格式/校对） |
| 校对无效 | `mode=6`（及对轴类场景）需传 `source_text` |
| lang 选错 | 按真实语种传 `zh` / `yue` / `en` / `ja` / `ko` / `other` |
| 只看 HTTP 200 | 不够，必须判断响应体 `code == 200` |
| 异步收不到回调 | 确认 `notify_url` 公网可达；联调可用 [webhook.site](https://webhook.site/) |
| 回调当成 JSON 解析失败 | 回调多为表单 POST，用 form/`$_POST`；`otherdata` 可能是 JSON 字符串需再解析 |
| 回调成功却判失败 | 回调成功 `errcode` 为 **`0`**，失败为 `1001` |

---

## 十、快速对照

| 模式 | type | 立即响应 | 结果获取 |
|------|------|----------|----------|
| 异步 | 1（默认） | `code:200` + `data.taskid` | `notify_url`：`errcode` / `text` / `otherdata` |
| 同步 | 2 | 直接返回结果（音频须 &lt; 10 分钟） | 看同步响应体 |
| stems | 1 / 2 | — | 极速 / 更高准确率 |
| mode | 1～7 | — | 文本 / SRT / VTT / ASS / TSV / 字幕校对 / 声音校对 |
