我的实战记录2!零基础用AI搭建专属数字人智能体之接入自己的声音

续篇:给数字人智能体接入声音克隆|让数字人用上我自己的音色

博客园续篇,承接上一篇《我的实战记录!零基础用hihookeji API搭建专属数字人智能体》,第一人称开发踩坑记录

上一篇我已经跑通整套数字人链路:用户提问 → 大模型输出文字 → 生成音频 → 提交数字人API,输出对口型讲解视频。

但是有个很别扭的点:之前我只能用平台自带的通用TTS音色,听上去就是标准机器音。我就想,如果数字人说话的声音是我本人的声音,整个智能体的代入感直接拉满。

于是我继续折腾创客API的声音克隆V3接口,把声音克隆完整接入到我现有的数字人智能体流程里面。本篇是实战续篇,记录我踩过的坑、完整流程、改造后的Python代码,把整套链路升级为:

用户输入问题 → 大模型输出回答文本 → 调用声音克隆API生成我的专属音色音频 → 获取公网音频url → 调用数字人合成接口生成视频 → 获取成品数字人视频

⚠️重要提醒:声音克隆涉及个人声音版权、合规风险,只允许克隆自己本人的声音,不要拿别人的录音做克隆,遵守平台协议和法律要求。

一、先搞懂这个声音克隆接口,我踩的第一个大坑

接口地址:[https://api.hihookeji.com/api/tts/clonevoicev3](https://api.hihookeji.com/api/tts/clonevoicev3)

我最开始以为随便丢一段录音上去,输入想要朗读的文字,就能直接出克隆语音。结果直接报错,反复调试半个多小时。

这里有一条硬性规则,文档写的很明确,但是很容易被忽略:

用于克隆的参考音频,朗读的内容,必须是平台指定的固定文案,同时ref_text参数要填一模一样的文字,否则克隆直接失败。

二选一,中文或者英文:
中文:我的声音将用于平台克隆,并合法使用,为自己的行为负责
英文:My voice will be used for platform cloning , and I take responsibility for my actions

而且参考音频的时长要求 5‑14秒,MP3格式,不能太长也不能太短。

简单说,我要自己录一段音频,认认真真朗读上面这句中文,保存mp3,上传到OSS拿到公网source_audio_url,请求参数里面ref_text填上一模一样这句话,克隆才会生效。

计费、权限、免费额度(我实测)

  1. 必须实名认证,和数字人接口一样,不实名调用直接失败。
  2. 注册赠送5次免费额度,可以拿来调试链路。
  3. 计费:0.02元/100字,不足100字按100字算,标点符号也算字数,单次合成文本上限2000字。
  4. 两种模式:
    • type=1异步模式:提交任务,通过notify_url回调拿音频文件,适合长文本。
    • type=2同步模式:直接返回base64编码音频,适合本地调试,不用公网回调地址,开发阶段我优先用这个模式。

开发建议:本地调试直接用type=2同步返回,拿到base64,解码保存成本地mp3,再上传OSS拿到公网链接,供给后面数字人接口使用;正式上线业务改用异步回调模式。

二、完整业务链路升级对比

旧流程(上一篇)

用户提问 → LLM输出回答 → 普通系统TTS生成音频 → OSS上传音频拿到audio_url → 数字人合成 → 输出视频

升级之后新完整链路(本篇)

用户提问 → LLM输出回答文本 → 声音克隆V3接口(type=2同步)返回base64音频 → base64解码保存mp3文件 → 把mp3上传对象存储OSS,获取公网可访问audio_url → 传入数字人V1/V3合成接口 → 等待任务完成,拿到带自己声音的数字人视频。

划重点!声音克隆接口返回的音频,不管是同步base64还是异步回调得到的mp3文件,不能直接把本地文件丢给数字人接口,依旧要上传OSS拿到公网https链接,这是之前踩过的老坑,千万不要忘记。

三、前期准备工作

  1. 账号已经实名(数字人和声音克隆都依赖实名同一个账号)。
  2. 录制参考音频mp3:5‑14秒,朗读那句固定中文声明,无背景噪音,人声清晰。上传OSS拿到公网source_audio_url。
  3. 复用之前的OSS存储空间:保存克隆出来的语音文件。
  4. 复用之前数字人模块代码,我们只新增声音克隆函数,尽量做到代码解耦。

四、Python完整改造代码,整合声音克隆+原有数字人逻辑

说明:

  1. 新增clone_voice_v3函数,type=2同步模式,直接拿到base64音频。适合本地调试,不需要公网回调notify_url。
  2. 演示base64解码保存本地mp3。实际项目中,你需要再写一段上传OSS逻辑,拿到公网audio_url。
  3. 保留上一篇原有数字人提交任务、轮询查询任务的代码,做无缝衔接。
import requests
import json
import time
import base64
import os

# ====================== 配置区,全部改成你自己的 ======================
TOKEN = "你的创客API密钥key"
# 声音克隆V3接口
CLONE_VOICE_API = "[https://api.hihookeji.com/api/tts/clonevoicev3](https://api.hihookeji.com/api/tts/clonevoicev3)"
# 数字人相关接口
DIGITAL_HUMAN_API = "[https://api.hihookeji.com/api/humanmeta/index](https://api.hihookeji.com/api/humanmeta/index)"
QUERY_TASK_API = "数字人任务查询接口地址"

# 【重要】参考音频:自己录制的5‑14秒mp3,朗读固定声明,上传到OSS
SOURCE_AUDIO_URL = "[https://xxx.oss](https://xxx.oss)‑cn‑beijing.aliyuncs.com/ref_voice.mp3"
# 固定参考文案,音频朗读的内容必须和这个完全一致!
REF_TEXT = "我的声音将用于平台克隆,并合法使用,为自己的行为负责"

# 数字人模板视频公网地址
TEMPLATE_VIDEO_URL = "[https://xxx.oss](https://xxx.oss)‑cn‑beijing.aliyuncs.com/template.mp4"
# 本地调试notify_url随便填,生产环境替换为公网回调
NOTIFY_URL = "[https://xxx.com/api/digital_human_callback](https://xxx.com/api/digital_human_callback)"

# 保存克隆音频的本地临时目录
TEMP_AUDIO_DIR = "./temp_audio"
os.makedirs(TEMP_AUDIO_DIR, exist_ok=True)
# ====================================================================

def llm_get_answer(user_question: str) -> str:
    """模拟大模型,真实项目替换为豆包/通义千问等大模型API调用"""
    print(f"\n【用户提问】{user_question}")
    answer_text = f"大家好,接下来我为你解答:{user_question}。这是使用我克隆声音生成的数字人口播讲解。"
    print(f"【大模型输出文本】{answer_text}")
    return answer_text

def clone_voice_v3(source_audio_url: str, target_text: str, ref_text: str, speed=1.0, pitch=1.0):
    """
    声音克隆V3,type=2同步模式,返回base64音频,适合本地调试
    :param source_audio_url:参考音频公网url,5‑14秒mp3
    :param target_text:要朗读的目标文本,最大2000字
    :param ref_text:参考音频朗读的固定文案,必须和录音内容完全一致
    :param speed:语速
    :param pitch:语调
    :return:成功返回音频base64字符串,失败返回None
    """
    payload = {
        "key": TOKEN,
        "source_audio_url": source_audio_url,
        "target_text": target_text,
        "ref_text": ref_text,
        "speed": speed,
        "pitch": pitch,
        "type": 2,  # type=2同步返回base64音频,本地调试首选
        "notify_url": "[http://127.0.0.1/dummy](http://127.0.0.1/dummy)"  # type=2同步模式notify_url为占位,随便填
    }
    headers = {
        "Content‑Type": "application/json"
    }
    resp = requests.post(CLONE_VOICE_API, json=payload, headers=headers)
    res_json = resp.json()
    print("\n【声音克隆接口返回】", json.dumps(res_json, ensure_ascii=False))
    errcode = res_json.get("errcode")
    if errcode == 200:
        audio_b64 = res_json["data"]["audio_base64"]
        return audio_b64
    else:
        print(f"❌声音克隆失败:{res_json.get('msg')}")
        return None

def save_base64_audio(base64_str, save_path):
    """base64解码,保存为本地mp3文件"""
    audio_bytes = base64.b64decode(base64_str)
    with open(save_path, "wb") as f:
        f.write(audio_bytes)
    print(f"✅克隆语音已经保存本地:{save_path}")

def submit_digital_human_task(audio_url: str, video_url: str, notify_url: str):
    """提交数字人合成任务(沿用第一篇代码)"""
    headers = {
        "Token": TOKEN,
        "Content‑Type": "application/json"
    }
    payload = {
        "audio_url": audio_url,
        "video_url": video_url,
        "notify_url": notify_url
    }
    resp = requests.post(DIGITAL_HUMAN_API, json=payload, headers=headers)
    res_json = resp.json()
    print("\n【数字人任务提交返回】", json.dumps(res_json, ensure_ascii=False))
    if res_json.get("code") == 0:
        task_id = res_json["data"]["task_id"]
        print(f"✅数字人任务提交成功 task_id={task_id}")
        return task_id
    else:
        print("❌数字人任务提交失败", res_json)
        return None

def query_task_status(task_id):
    """轮询查询数字人任务状态(沿用第一篇)"""
    headers = {"Token": TOKEN}
    params = {"task_id": task_id}
    resp = requests.get(QUERY_TASK_API, params=params, headers=params)
    return resp.json()

if __name__ == "__main__":
    question = "什么是数字人智能体,接入声音克隆之后有什么效果?"
    # 1.大模型生成回答文本
    answer_text = llm_get_answer(question)

    # 2.调用声音克隆接口,生成我自己音色的音频base64
    audio_b64_data = clone_voice_v3(
        source_audio_url=SOURCE_AUDIO_URL,
        target_text=answer_text,
        ref_text=REF_TEXT,
        speed=1.0
    )
    if not audio_b64_data:
        print("声音克隆失败,程序退出")
        exit(-1)

    # 3.base64解码保存本地mp3
    temp_mp3_path = os.path.join(TEMP_AUDIO_DIR, "clone_out.mp3")
    save_base64_audio(audio_b64_data, temp_mp3_path)

    # =================重要====================
    # 这里你需要写代码,把 temp_mp3_path 这个本地文件上传到OSS对象存储
    # 获取公网  https://xxx/clone_out.mp3 ,赋值给下面的 AUDIO_PUBLIC_URL
    AUDIO_PUBLIC_URL = "填写你上传OSS之后得到公网音频url"

    # 4.提交数字人视频合成任务
    taskid = submit_digital_human_task(AUDIO_PUBLIC_URL, TEMPLATE_VIDEO_URL, NOTIFY_URL)
    if not taskid:
        exit(-1)

    # 5.轮询等待数字人视频渲染完成
    print("\n⏳数字人视频渲染中,请等待...")
    for i in range(20):
        time.sleep(15)
        task_result = query_task_status(taskid)
        status = task_result["data"]["status"]
        print(f"【第{i+1}次轮询,当前状态:{status}】")
        if status == "success":
            final_video_url = task_result["data"]["video_url"]
            print(f"\n🎉全部流程完成!带克隆声音的数字人视频地址:{final_video_url}")
            break
        elif status == "fail":
            print(f"\n❌数字人合成失败,原因:{task_result['data']['msg']}")
            break

五、我开发过程踩坑清单(血泪总结)

  1. ref_text 和录音内容必须一字不差

坑现象:调用接口直接返回克隆失败。
原因:录音读的句子,和参数ref_text文本有一个标点、空格不一样,直接克隆失败。录制音频的时候,就完整朗读平台给出的那一句声明,不要自己修改。

  1. 参考音频时长不能超出5‑14秒范围

音频太短或者太长都会报错,录制的时候控制语速,刚好读完那一句声明即可,不要加多余开场白、背景音乐,尽量安静环境录音。

  1. 同步type=2拿到base64,解码之后只是本地文件,不能直接传给数字人接口

我一开始偷懒,直接把本地路径填进audio_url,数字人接口一直报错。记住老规矩:所有音频视频素材,必须公网https可访问url,本地文件一律不行,必须上传OSS。

  1. 文本字数上限2000字,长文本要做截断拆分
    单次target_text最多2000字符,如果大模型输出回答很长,要拆成多次声音克隆任务,分段生成音频,再拼接音频,再给到数字人接口。
  2. 鉴权key放在post请求body里面,不是header
    这点和数字人接口不一样!声音克隆接口的密钥key是放在POST请求体参数,而不是http请求头,我一开始沿用数字人的习惯放header,一直鉴权失败,排查很久。
  3. 同步模式type=2下notify_url可以随便填占位,但是参数不能省略
    即使同步不走回调,notify_url参数依旧是必填,不能不传,随便写个占位url即可。
  4. 合规红线:只能克隆本人声音
    不要去截取网上别人的音频做声音克隆,这涉及肖像权、声音权侵权,自己做个人项目也一定要遵守。

六、异步模式简单说明(适合生产环境)

如果做正式业务,文本比较长,推荐用type=1异步模式。

  • notify_url填写你自己公网服务回调地址。
  • 提交成功返回taskid,平台合成完毕后,以multipart/form-data形式把mp3音频文件POST推送到你的回调地址。
  • 收到音频文件之后直接存入OSS,拿到公网url,继续交给数字人接口。

Flask简易回调接收音频示例(异步模式)

from flask import Flask, request
app = Flask(__name__)

@app.route("/api/clone_callback", methods=["POST"])
def clone_callback():
    taskid = request.form.get("taskid")
    code = request.form.get("code")
    if "target_file" in request.files:
        audio_file = request.files["target_file"]
        audio_file.save("./temp_clone.mp3")
        # 此处写逻辑上传OSS
        print(f"收到克隆音频 taskid={taskid}")
    return {"code": 0, "msg": "ok"}

if __name__ == '__main__':
    app.run(host="0.0.0.0", port=8001)

七、成本与效果体验小结

新账号送5次免费次数,我先用免费次数调试通整个链路。正式使用0.02元每百字,生成一段几百字的口播音频成本几分钱。

实际体验:只要参考音频录制清晰、无杂音,克隆出来音色相似度很高。但是语速、情绪起伏和原始录音会有差异,可以通过speed、pitch参数微调语速语调。

八、下一步我打算继续迭代的点

  1. 增加音频自动拼接逻辑,大模型输出超长文本自动拆分多次克隆语音,再拼接完整音频。
  2. 封装OSS上传函数,代码一体化:base64音频自动上传OSS,直接返回公网url,省去手动处理。
  3. 增加停顿标签、多音字标签,优化朗读效果,接口支持<break time="2s"/>停顿标签,<phoneme ph="xxx">多音字矫正标签,可以直接写进target_text里面。
  4. 把整套大模型‑声音克隆‑数字人封装成完整接口,做简易web页面,输入问题一键输出属于自己音色的数字人视频。

总结

到这里,整套数字人智能体就彻底完整了。
上一篇完成了:大模型思考 + 普通TTS + 数字人视频。
本篇续篇升级:大模型思考 + 声音克隆复刻自己音色 + 数字人对口型视频。

现在,我可以输入一个问题,AI思考回答,用我的声音朗读出来,数字人视频人物跟着我的克隆声音对口型输出完整视频。整个过程全部API化,不需要本地训练任何语音、数字人模型,个人开发者完全可以跑起来。

提示:本篇代码仅用于技术学习,使用声音克隆功能务必遵守平台协议,严禁克隆他人声音。

posted @ 2026-10-13 15:44  开发攻城狮008  阅读(2)  评论(0)    收藏  举报