声音克隆 API V3 接入教程
在线效果体验:https://ai.hihookeji.com
接口地址:
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/ 获取自己的唯一 URL,将该地址填入 notify_url。提交任务后可在网页上直接查看服务端推送的表单字段与 target_file 文件,无需先部署自己的回调服务。
三、参考音频文案(必须严格一致)
source_audio_url 对应音频中念出的内容,以及参数 ref_text,必须与下列文案完全一致,否则克隆会失败:
- 中文:
我的声音将用于平台克隆,并合法使用,为自己的行为负责
- 英文:
My voice will be used for platform cloning , and I take responsibility for my actions
四、文本标签能力
可在 target_text 中使用下列标签。
4.1 停顿标签
支持秒(s)与毫秒(ms),单次停顿 最大不超过 10 秒,否则失败。
如果每天都是全新的一天。
表示停顿50毫秒
4.2 多音字 / 发音矫正
每天早起照镜子都崩溃。
多音字:重要处理。
ph中音标与声调之间有空格,如wan 3、gan 4、chong 3。
4.3 读法标签(say-as)
| interpret-as | 含义 |
|---|---|
| number | 数字读法 |
| time | 时间读法 |
| date | 日期读法 |
| value | 数值读法 |
| telephone | 手机号 / 电话读法 |
4.4 完整示例
金额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 |
{
"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 解码再保存为文件。
{
"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'] 接收;失败时无此字段 |
六、接入流程建议
- 准备合规参考音频(5~14 秒,MP3),念上述指定文案之一,公网可访问 URL。
- 申请 API Key,作为请求体字段
key与其它参数一起提交。
- 字数 ≤1000 且需立即拿结果:可用
type=2同步;字数较多或需稳定投递:用type=1异步。联调回调时可先用 webhook.site 获取唯一接收地址填入notify_url。
- 异步场景下用
data.taskid关联提交与回调结果。
- 同步场景对
data.audio_base64解码后落盘。
七、Python 完整代码
依赖:requests(pip install requests)
# -*- 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)
九、Java 完整代码
依赖:JDK 8+,使用内置 HttpURLConnection,无需第三方库。
若使用 Maven,也可自行替换为 OkHttp / Apache HttpClient。
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 < 300)
? conn.getInputStream()
: conn.getErrorStream();
String resp = readFully(is);
conn.disconnect();
return resp;
}
/**
* 将 type=2 的 audio_base64 解码保存为 mp3
*/
public static Path saveBase64Audio(String responseJson, String saveDir) throws IOException {
int code = extractInt(responseJson, "code");
if (code != 200) {
String msg = extractString(responseJson, "msg");
throw new IOException("合成失败: " + msg);
}
String audioB64 = extractNestedString(responseJson, "audio_base64");
if (audioB64 == null || audioB64.isEmpty() || "null".equals(audioB64)) {
throw new IOException("响应中缺少 data.audio_base64");
}
Path dir = Paths.get(saveDir);
if (!Files.exists(dir)) {
Files.createDirectories(dir);
}
byte[] audioBytes = Base64.getDecoder().decode(audioB64);
String taskid = extractNestedString(responseJson, "taskid");
String name = (taskid != null && !taskid.isEmpty())
? taskid + ".mp3"
: LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss")) + ".mp3";
Path path = dir.resolve(name);
Files.write(path, audioBytes);
return path;
}
// --------------------- JSON 构造 / 简易解析 ---------------------
private static String toJson(CloneRequest r) {
StringBuilder sb = new StringBuilder();
sb.append("{");
sb.append(""key":").append(quote(API_KEY)).append(",");
sb.append(""source_audio_url":").append(quote(r.sourceAudioUrl)).append(",");
sb.append(""target_text":").append(quote(r.targetText)).append(",");
sb.append(""notify_url":").append(quote(r.notifyUrl)).append(",");
sb.append(""ref_text":").append(quote(r.refText)).append(",");
sb.append(""speed":").append(r.speed).append(",");
sb.append(""pitch":").append(r.pitch).append(",");
sb.append(""type":").append(r.type).append(",");
sb.append(""mtype":").append(r.mtype);
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 urlEncode(String s) {
try {
return java.net.URLEncoder.encode(s, "UTF-8");
} catch (UnsupportedEncodingException e) {
return s;
}
}
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);
}
private static int extractInt(String json, String key) {
String v = extractString(json, key);
try {
return Integer.parseInt(v);
} catch (Exception e) {
return -1;
}
}
private static String extractString(String json, String key) {
String pattern = """ + key + """;
int i = json.indexOf(pattern);
if (i < 0) {
return null;
}
int colon = json.indexOf(':', i + pattern.length());
if (colon < 0) {
return null;
}
int start = colon + 1;
while (start < json.length() && Character.isWhitespace(json.charAt(start))) {
start++;
}
if (start >= json.length()) {
return null;
}
if (json.charAt(start) == '"') {
int end = start + 1;
StringBuilder sb = new StringBuilder();
while (end < json.length()) {
char c = json.charAt(end);
if (c == '\' && end + 1 < json.length()) {
sb.append(json.charAt(end + 1));
end += 2;
continue;
}
if (c == '"') {
break;
}
sb.append(c);
end++;
}
return sb.toString();
}
int end = start;
while (end < json.length()) {
char c = json.charAt(end);
if (c == ',' || c == '}' || c == ']') {
break;
}
end++;
}
return json.substring(start, end).trim();
}
private static String extractNestedString(String json, String key) {
return extractString(json, key);
}
// --------------------- 异步回调 Servlet 示例(可选) ---------------------
/*
* 若使用 Servlet 容器,可参考下列伪代码:
*
* @WebServlet("/Notify")
* public class NotifyServlet extends HttpServlet {
* protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
* // 表单字段: taskid / msg / errcode(成功0,失败1001)
* // multipart: Part file = req.getPart("target_file");
* }
* }
*/
public static void main(String[] args) throws Exception {
// 示例 1:异步 type=1
CloneRequest asyncReq = new CloneRequest();
asyncReq.sourceAudioUrl = "https://a.b.c/1.mp3";
asyncReq.targetText = "你好,这是克隆语音。今天天气不错。";
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 唯一地址验证是否已推送;正式环境再换成自己的接收服务 |
| 回调当成 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 解码落盘 |

粤ICP备19001875号-6