技术开发记录:创客API-声音克隆开发API接口接入
# 声音克隆 API V3 接入教程 > 接口地址:`https://api.hihookeji.com/api/tts/clonevoicev3` > 返回格式:`application/json` > 请求方式:`HTTP POST` --- ## 一、接口概述 本接口根据一段 **5~14 秒** 的参考音频(建议 MP3)与指定文案,克隆音色并合成目标文本语音。 | 项目 | 说明 | |------|------| | 接口地址 | `https://api.hihookeji.com/api/tts/clonevoicev3` | | 认证方式 | 请求体字段 `key`(你的 API 密钥,与业务参数一并提交) | | 请求示例 | `https://api.hihookeji.com/api/tts/clonevoicev3` | | Content-Type | `application/json`(推荐)或 `application/x-www-form-urlencoded` | | 业务判断 | 统一用响应字段 **`code`**:`200` 成功 | | HTTP 状态码 | 请以响应体 `code` 为准 | --- ## 二、请求参数 | 参数名 | 类型 | 必填 | 描述 | 示例 | |--------|------|------|------|------| | key | string | 是 | API 密钥,放在请求体中与其它参数一起提交 | `你的密钥` | | source_audio_url | string | 是 | 参考音频公网 URL,须为标准 **mp3 / wav**,且 URL **以 `.mp3` 或 `.wav` 结尾**;时长 **5~14 秒**;音频中读的文案必须为下方指定文案 | `https://a.b.c/1.mp3` | | target_text | string | 是 | 要合成的目标文本,单次不超过 **2000** 字;超过 **1000** 字建议用异步(type=1) | `我是克隆的文字` | | notify_url | string | 是 | 合成结果通知地址(异步回调接收地址,需公网可达) | 见下方「回调测试」 | | ref_text | string | 是 | 参考音频文案,必须为下方两种之一 | 见下方说明 | | speed | float | 否 | 语速,默认 `1.0` | `1` | | pitch | float | 否 | 语调,默认 `1.0` | `1` | | type | int | 否 | 结果返回类型,默认异步:`1` 异步文件回调;`2` 同步返回 Base64 音频 | `1` | | mtype | int | 否 | `1` 单人;`2` 多角色 | `1` | **回调测试**:联调异步回调时,建议先打开 [https://webhook.site/](https://webhook.site/) 获取自己的唯一 URL,将该地址填入 `notify_url`。提交任务后可在网页上直接查看服务端推送的表单字段与 `target_file` 文件,无需先部署自己的回调服务。 --- ## 三、参考音频文案(必须严格一致) `source_audio_url` 对应音频中念出的内容,以及参数 `ref_text`,**必须**与下列文案完全一致,否则克隆会失败: 1. 中文:`我的声音将用于平台克隆,并合法使用,为自己的行为负责` 2. 英文:`My voice will be used for platform cloning , and I take responsibility for my actions` --- ## 四、文本标签能力 可在 `target_text` 中使用下列标签。 ### 4.1 停顿标签 支持秒(s)与毫秒(ms),单次停顿 **最大不超过 10 秒**,否则失败。 ```text 如果每天都是全新的一天。 表示停顿50毫秒 ``` ### 4.2 多音字 / 发音矫正 ```text 每天早起照镜子都崩溃。 多音字:重要处理。 ``` > `ph` 中音标与声调之间有空格,如 `wan 3`、`gan 4`、`chong 3`。 ### 4.3 读法标签(say-as) | interpret-as | 含义 | |--------------|------| | number | 数字读法 | | time | 时间读法 | | date | 日期读法 | | value | 数值读法 | | telephone | 手机号 / 电话读法 | ### 4.4 完整示例 ```text 金额123436511.562254, 数值是-1204455, 停顿5秒, 多音字:重要处理。 现在的时间是:14:05, Today is 25-02-01. 我的电话: (888) 555-1212 ``` --- ## 五、返回结果说明 ### 5.1 异步提交成功(type=1) 立即返回任务受理结果;`data.audio_base64` 为 `null`,音频通过 `notify_url` 回调。 | 字段 | 说明 | |------|------| | code | 业务状态码,`200` 成功 | | msg | 说明信息 | | data.taskid | 任务 ID,回调关联用 | | data.consume | 消耗点数 | | data.audio_base64 | 异步为 `null` | | exec_time | 接口耗时(秒) | | ip | 请求方 IP | ```json { "code": 200, "msg": "success", "data": { "taskid": "1784017039696108", "consume": 20, "audio_base64": null }, "exec_time": 0.097585, "ip": "14.19.93.116" } ``` ### 5.2 同步返回(type=2) 同步成功时 `data.audio_base64` 为音频 Base64 字符串。**必须先 Base64 解码再保存为文件。** ```json { "code": 200, "msg": "success", "data": { "taskid": "1784017039913816", "consume": 20, "audio_base64": "SUQzBAAAAAALFVRTo......=" }, "exec_time": 0.936593, "ip": "14.19.93.116" } ``` ### 5.3 异步回调 Notify(type=1) 服务端合成完成后,以 **表单 POST** 向 `notify_url` 推送(成功带文件时为 `multipart/form-data`,失败多为 `application/x-www-form-urlencoded`),**不是** JSON Body。 | 字段 | 说明 | |------|------| | errcode | **`0` 成功**;**`1001` 失败**(与接口提交响应的 `code: 200` 不同,回调请按此处判断) | | taskid | 任务 ID | | msg | 说明文案 | | target_file | 音频文件(mp3/wav),PHP 可用 `$_FILES['target_file']` 接收;失败时无此字段 | --- ## 六、接入流程建议 1. 准备合规参考音频(5~14 秒,MP3),念上述指定文案之一,公网可访问 URL。 2. 申请 API Key,作为请求体字段 `key` 与其它参数一起提交。 3. 字数 ≤1000 且需立即拿结果:可用 `type=2` 同步;字数较多或需稳定投递:用 `type=1` 异步。联调回调时可先用 [webhook.site](https://webhook.site/) 获取唯一接收地址填入 `notify_url`。 4. 异步场景下用 `data.taskid` 关联提交与回调结果。 5. 同步场景对 `data.audio_base64` 解码后落盘。 --- ## 七、Python 完整代码 依赖:`requests`(`pip install requests`) ```python # -*- coding: utf-8 -*- """ 声音克隆 API V3 - Python 完整示例 接口: https://api.hihookeji.com/api/tts/clonevoicev3 """ import os import base64 import json from datetime import datetime import requests API_URL = "https://api.hihookeji.com/api/tts/clonevoicev3" API_KEY = "你的密钥" # 替换为真实密钥 # 参考音频文案(必须与音频内容一致,二选一) REF_TEXT_CN = "我的声音将用于平台克隆,并合法使用,为自己的行为负责" REF_TEXT_EN = ( "My voice will be used for platform cloning , " "and I take responsibility for my actions" ) def clone_voice( source_audio_url, target_text, notify_url, ref_text=REF_TEXT_CN, speed=1.0, pitch=1.0, result_type=1, mtype=1, timeout=120, ): """ 调用声音克隆 V3 接口。 :param source_audio_url: 参考音频公网 URL(mp3,5~14 秒) :param target_text: 目标合成文本(≤2000 字) :param notify_url: 异步回调地址(type=1 必填且需可公网访问) :param ref_text: 参考音频文案(必须为指定文案) :param speed: 语速,默认 1.0 :param pitch: 语调,默认 1.0 :param result_type: 1=异步回调,2=同步 Base64 :param mtype: 1=单人,2=多角色 :param timeout: HTTP 超时秒数(同步建议调大) :return: 接口 JSON 字典 """ url = API_URL payload = { "key": API_KEY, "source_audio_url": source_audio_url, "target_text": target_text, "notify_url": notify_url, "ref_text": ref_text, "speed": speed, "pitch": pitch, "type": result_type, "mtype": mtype, } resp = requests.post( url, json=payload, headers={"Content-Type": "application/json"}, timeout=timeout, ) resp.raise_for_status() return resp.json() def save_base64_audio(result, save_dir="./output"): """ 将 type=2 同步返回的 Base64 音频解码并保存为 mp3。 """ if int(result.get("code") or 0) != 200: raise RuntimeError(f"合成失败: {result.get('msg')}") data = result.get("data") or {} audio_b64 = data.get("audio_base64") if not audio_b64: raise RuntimeError("响应中缺少 data.audio_base64") os.makedirs(save_dir, exist_ok=True) audio_bytes = base64.b64decode(audio_b64) timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") taskid = data.get("taskid") or timestamp save_path = os.path.join(save_dir, f"{taskid}.mp3") with open(save_path, "wb") as f: f.write(audio_bytes) return save_path # --------------------------------------------------------------------------- # Flask 异步回调接收示例(可选,部署到公网后作为 notify_url) # 安装: pip install flask # --------------------------------------------------------------------------- def create_notify_app(save_dir="./callback_audio"): from flask import Flask, request, jsonify app = Flask(__name__) os.makedirs(save_dir, exist_ok=True) @app.route("/Notify", methods=["POST"]) def notify(): # 回调为表单字段(有文件时是 multipart),不是 JSON Body taskid = request.form.get("taskid") msg = request.form.get("msg") errcode = request.form.get("errcode") # 成功 0,失败 1001 if "target_file" in request.files: f = request.files["target_file"] if f and f.filename: ext = os.path.splitext(f.filename)[1] or ".mp3" path = os.path.join(save_dir, f"{taskid}{ext}") f.save(path) return jsonify({"ok": True, "taskid": taskid, "saved": path, "errcode": errcode}) return jsonify({"ok": False, "taskid": taskid, "msg": msg, "errcode": errcode}) return app if __name__ == "__main__": # ---------- 示例 1:异步提交(type=1)---------- async_result = clone_voice( source_audio_url="https://a.b.c/1.mp3", target_text='你好,这是克隆语音。今天天气不错。', notify_url="https://webhook.site/你的唯一ID", ref_text=REF_TEXT_CN, result_type=1, mtype=1, ) print("异步提交:", json.dumps(async_result, ensure_ascii=False, indent=2)) # 成功时 code==200,记录 data.taskid;回调可在 webhook.site 页面查看 # ---------- 示例 2:同步返回 Base64(type=2)---------- sync_result = clone_voice( source_audio_url="https://a.b.c/1.mp3", target_text="我是克隆的文字", notify_url="https://webhook.site/你的唯一ID", # 同步模式仍按文档传 notify_url ref_text=REF_TEXT_CN, result_type=2, timeout=180, ) print("同步响应 code:", sync_result.get("code"), "type:", type(sync_result.get("code")), "msg:", sync_result.get("msg")) print("data keys:", list((sync_result.get("data") or {}).keys())) print("audio_base64 是否有值:", bool((sync_result.get("data") or {}).get("audio_base64"))) if int(sync_result.get("code") or 0) == 200: try: path = save_base64_audio(sync_result) print("音频已保存:", path) except Exception as e: print("保存失败:", repr(e)) print("完整 data:", json.dumps(sync_result.get("data"), ensure_ascii=False)[:500]) # ---------- 启动回调服务(需要时取消注释)---------- # app = create_notify_app() # app.run(host="0.0.0.0", port=8080) ``` --- ## 八、PHP 完整代码 需开启 `curl` 扩展。回调示例可直接作为独立 `notify.php` 部署。 ```php API_KEY, 'source_audio_url' => $params['source_audio_url'], 'target_text' => $params['target_text'], 'notify_url' => $params['notify_url'], 'ref_text' => isset($params['ref_text']) ? $params['ref_text'] : REF_TEXT_CN, 'speed' => isset($params['speed']) ? $params['speed'] : 1.0, 'pitch' => isset($params['pitch']) ? $params['pitch'] : 1.0, 'type' => isset($params['type']) ? (int)$params['type'] : 1, 'mtype' => isset($params['mtype']) ? (int)$params['mtype'] : 1, ]; $ch = curl_init($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; } /** * 保存 type=2 返回的 Base64 音频 * * @param array $result 接口响应 * @param string $saveDir 目录 * @return string 文件路径 * @throws Exception */ function saveBase64Audio(array $result, $saveDir = './output') { if (!isset($result['code']) || (int)$result['code'] !== 200) { $msg = isset($result['msg']) ? $result['msg'] : 'unknown'; throw new Exception('合成失败: ' . $msg); } if (empty($result['data']['audio_base64'])) { throw new Exception('响应中缺少 data.audio_base64'); } if (!is_dir($saveDir) && !mkdir($saveDir, 0755, true)) { throw new Exception('无法创建目录: ' . $saveDir); } $bytes = base64_decode($result['data']['audio_base64'], true); if ($bytes === false) { throw new Exception('Base64 解码失败'); } $name = !empty($result['data']['taskid']) ? $result['data']['taskid'] : date('Ymd_His'); $path = rtrim($saveDir, '/\\') . DIRECTORY_SEPARATOR . $name . '.mp3'; if (file_put_contents($path, $bytes) === false) { throw new Exception('写入文件失败: ' . $path); } return $path; } // ======================== 调用示例 ======================== try { // 示例 1:异步提交 type=1 $asyncResult = cloneVoice([ 'source_audio_url' => 'https://a.b.c/1.mp3', 'target_text' => '你好,这是克隆语音。今天天气不错。', 'notify_url' => 'https://webhook.site/你的唯一ID', 'ref_text' => REF_TEXT_CN, 'type' => 1, 'mtype' => 1, ]); echo "异步提交:\n" . json_encode($asyncResult, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; // 示例 2:同步 Base64 type=2 $syncResult = cloneVoice([ 'source_audio_url' => 'https://a.b.c/1.mp3', 'target_text' => '我是克隆的文字', 'notify_url' => 'https://webhook.site/你的唯一ID', 'ref_text' => REF_TEXT_CN, 'type' => 2, 'mtype' => 1, ], 180); echo "同步 code: " . $syncResult['code'] . ", msg: " . $syncResult['msg'] . "\n"; if ((int)$syncResult['code'] === 200) { $path = saveBase64Audio($syncResult); echo "音频已保存: {$path}\n"; } } catch (Exception $e) { echo '错误: ' . $e->getMessage() . "\n"; } /* ======================== notify.php 异步回调接收 ======================== 将下方代码单独保存为 notify.php,部署到公网,地址填入 notify_url。 true, 'taskid' => $taskid, 'saved' => $path, 'errcode' => $errcode], JSON_UNESCAPED_UNICODE); exit; } echo json_encode([ 'ok' => false, 'taskid' => $taskid, 'msg' => $msg, 'errcode' => $errcode, ], JSON_UNESCAPED_UNICODE); */ ``` --- ## 九、Java 完整代码 依赖:JDK 8+,使用内置 `HttpURLConnection`,无需第三方库。 若使用 Maven,也可自行替换为 OkHttp / Apache HttpClient。 ```java import java.io.*; import java.net.HttpURLConnection; import java.net.URL; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.Base64; /** * 声音克隆 API V3 - Java 完整示例 * 接口: https://api.hihookeji.com/api/tts/clonevoicev3 * * 说明:为减少依赖,本示例用字符串拼 JSON,并用简单方式解析 code / audio_base64。 * 生产环境建议改用 Jackson / Gson 等库。 */ public class CloneVoiceV3Demo { private static final String API_URL = "https://api.hihookeji.com/api/tts/clonevoicev3"; private static final String API_KEY = "你的密钥"; // 替换为真实密钥 public static final String REF_TEXT_CN = "我的声音将用于平台克隆,并合法使用,为自己的行为负责"; public static final String REF_TEXT_EN = "My voice will be used for platform cloning , and I take responsibility for my actions"; public static class CloneRequest { public String sourceAudioUrl; public String targetText; public String notifyUrl; public String refText = REF_TEXT_CN; public double speed = 1.0; public double pitch = 1.0; public int type = 1; // 1 异步,2 同步 Base64 public int mtype = 1; // 1 单人,2 多角色 } /** * 调用声音克隆 V3 */ public static String cloneVoice(CloneRequest req, int timeoutMs) throws IOException { String urlStr = API_URL; URL url = new URL(urlStr); 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 code = conn.getResponseCode(); InputStream is = (code >= 200 && code = json.length()) { return null; } if (json.charAt(start) == '"') { int end = start + 1; StringBuilder sb = new StringBuilder(); while (end 今天天气不错。"; asyncReq.notifyUrl = "https://webhook.site/你的唯一ID"; asyncReq.refText = REF_TEXT_CN; asyncReq.type = 1; asyncReq.mtype = 1; String asyncResp = cloneVoice(asyncReq, 60000); System.out.println("异步提交: " + asyncResp); // 示例 2:同步 type=2 CloneRequest syncReq = new CloneRequest(); syncReq.sourceAudioUrl = "https://a.b.c/1.mp3"; syncReq.targetText = "我是克隆的文字"; syncReq.notifyUrl = "https://webhook.site/你的唯一ID"; syncReq.refText = REF_TEXT_CN; syncReq.type = 2; syncReq.mtype = 1; String syncResp = cloneVoice(syncReq, 180000); System.out.println("同步响应: " + syncResp); if (extractInt(syncResp, "code") == 200) { Path path = saveBase64Audio(syncResp, "./output"); System.out.println("音频已保存: " + path.toAbsolutePath()); } } } ``` --- ## 十、常见问题 | 问题 | 处理建议 | |------|----------| | 克隆效果异常 / 失败 | 检查参考音频是否念指定文案,且 `ref_text` 与音频内容完全一致 | | 参考音频格式不对 | `source_audio_url` 必须是标准 **mp3** 或 **wav**;提示失败时可先用 ffmpeg 转码后再上传,例如:`ffmpeg -i 输入文件 -acodec libmp3lame -ar 44100 输出.mp3` | | 参考音频链接后缀不对 | URL 必须以 **`.mp3` 或 `.wav` 结尾**;常见 OSS 带鉴权参数的长链接(不以 `.mp3`/`.wav` 结尾)会失败,需换成可访问且后缀正确的直链,或转存后再提交 | | 参考音频时长 | 必须在 5~14 秒;URL 需公网可访问 | | 长文本 | 超过 1000 字建议 `type=1` 异步;单次最多 2000 字 | | 停顿过长 | `` 单次最大 10 秒 | | 只看 HTTP 200 | 不够,必须判断响应体 `code == 200` | | 同步音频无法播放 | 对 `data.audio_base64` **解码后再写文件**,不要直接把 Base64 字符串当二进制保存 | | 异步收不到回调 | 确认 `notify_url` 公网可达、支持 POST;联调可先用 [webhook.site](https://webhook.site/) 唯一地址验证是否已推送;正式环境再换成自己的接收服务 | | 回调当成 JSON 解析失败 | 回调是表单 POST,用 form/`$_POST` 取值,不要按 JSON Body 解析 | | 回调成功却判失败 | 回调成功 `errcode` 为 **`0`**(不是提交接口的 `code: 200`),失败为 `1001` | | 旧接口标签无效 | 请改用本教程新地址 `.../clonevoicev3` | --- ## 十一、快速对照 | 模式 | type | 立即响应 | 音频获取方式 | |------|------|----------|--------------| | 异步 | 1(默认) | `code:200`,`data.taskid`,`audio_base64: null` | `notify_url` 回调 multipart 文件 `target_file` | | 同步 | 2 | `code:200`,`data.audio_base64` 有值 | 本地 Base64 解码落盘 |