AI数字人克隆源码拆解:音色克隆与口型驱动工程化实践 简介这份 AI 数字人克隆完整源码面向具备 PHP 与前端基础的开发者、高校研究者及小型技术团队指向数字人形象还原、表情动作驱动与语音模仿等实现路径的学习与二次开发需求可服务于虚拟主播、在线客服、影视特效等场景的原型验证。压缩包共 1039 个文件体积约 6.46MB以 709 个 PHP 文件构成主体业务与接口逻辑配合 60 个 HTML 页面、24 个 JS 脚本与 24 个 JSON 配置承担交互与数据组织另有 png、jpg 等图像素材及 wxss、wxml 小程序端页面文件整体形成前后端与多端并存的目录结构。目前已有 592 人学习。读者可从中梳理数字人克隆的模块划分与调用关系对照深度学习特征提取、视觉捕捉、三维模型渲染等环节理解工程落地方式并以此为骨架做界面改造、功能裁剪或接入自有模型适合作为入门研读与定制开发的参考底本。1. 一段 30 秒的自拍怎么变成能反复出镜的数字分身先给结论这套 AI 数字人克隆源码不是几个 Gradio 脚本简单放在一起它把音色克隆、形象驱动、合成封装拆成了三条独立的推理链路再用一个任务状态机串起来。使用场景很具体——手上有几段手机拍的正脸口播素材希望让视频里这个人继续说新文案换语种、换语气都行嘴型和表情还要跟新音频对得上能批量输出成片不用每次都请真人重新录制。适合两类人一类是做企业宣传、课程录制、电商口播的团队有稳定出片需求但不想养一个拍摄组另一类是想在数字人方向做二次开发的工程师需要一个结构清晰、能改的工程底座。使用门槛也很现实——一张能撑住推理的显卡加上调参的耐心要改它至少得先看懂声音和形象这两条链各自吃什么格式的数据。2. 架构拆解四段工序怎么落到三块工程代码整个项目我一般按“四段工序、三块代码”去理解。四段工序是素材前处理 → 音色克隆与语音合成 → 形象驱动与口型生成 → 合成封装。三块代码是app/inference负责模型封装app/api负责对外接口app/core负责任务调度和状态流转。这个切法的好处是模型可以随便换只要接口签名不变上层业务代码基本不用动。真正容易别扭的地方在于很多人一上手就想把“声音”和“形象”当成一个整体去调结果两边参数互相拉扯改一处影响另一处最后不知道该怪谁。这套源码把两者彻底解耦语音合成产出一个 wav形象驱动只接收这个 wav 和一段参考视频各管各的。2.1 目录结构inference、service、web 三层是怎么切的先看目录。这份源码的骨架大致长这样权重目录单独留出来且默认不进版本库这点很关键否则仓库会被几十 GB 的文件撑爆digital-human-clone/ ├── configs/ │ ├── server.yaml # 服务端口、并发、临时目录 │ ├── tts.yaml # 语音链路参数 │ └── avatar.yaml # 形象链路参数 ├── models/ # 权重占位目录不入库 │ ├── tts/ │ └── talking-head/ ├── app/ │ ├── api/ # FastAPI 路由层只做参数校验和转发 │ │ ├── tts_router.py │ │ ├── avatar_router.py │ │ └── task_router.py │ ├── core/ # 任务状态机、队列、结果存储 │ │ ├── pipeline.py │ │ └── store.py │ ├── preprocess/ # 音频切片、视频抽帧、人脸检测 │ ├── inference/ # 模型加载与推理封装 │ └── postprocess/ # 帧序列 音频封装成 mp4 ├── web/ # 前端页面只管提交和轮询 ├── scripts/ # 权重下载、抽帧、批量任务脚本 ├── requirements.txt └── docker/分层逻辑很清楚api层薄只做三件事——收参数、校验、把任务丢进corecore层是黑匣子里面跑什么模型外面不用知道preprocess和postprocess是最容易被低估的两块实际跑下来前处理做得好不好直接决定克隆相似度是“像”还是“不太像”。configs三个 yaml 分开也是有意为之。语音链路常调的是语速、温度、参考音频长度形象链路常调的是批大小、插帧、人脸检测阈值。放一个文件里改 A 的时候总是误碰 B。2.2 技术选型声音走自回归、形象走扩散两条链别混着优化这套源码的模型组合常见做法是语音侧用自回归式的 TTSVITS、Bert-VITS2、GPT-SoVITS 这一类思路形象侧用扩散或 GAN 系的 talking-head 模型Wav2Lip、MuseTalk、SadTalker 这一路。两者优化目标完全不同硬凑一起调只会互相拖累。工序典型方案输入输出显存占用量级音频前处理librosa ffmpeg任意格式录音16k 单声道 wav无音色克隆 / TTS自回归 TTS 音色嵌入参考音频 文本目标音色 wav610 GB人脸检测对齐InsightFace / MediaPipe参考视频帧bbox 关键点12 GB口型驱动扩散 / GAN talking-head参考帧 wav帧序列820 GB合成封装ffmpeg帧序列 wavmp4无为什么不能混着优化语音链路的效果指标是“像不像这个人说话”形象链路是“嘴型对不对得上音频”。前者靠参考音频质量和音色嵌入的稳定性后者靠人脸对齐精度和时序一致性。你把语音温度调高音色会更生动但可能跑偏你把口型批大小调大显存直接爆。两个方向的参数根本不在一个语义空间里。提示如果你的显卡只有 12 GB 以下建议先按“语音链路和形象链路分阶段跑、跑完一段卸载一段”的方式配置别一上来就两个模型常驻。2.3 任务状态机一次克隆任务要经过的 6 个状态把长耗时任务做成异步状态机是这套源码比较工程化的一点。前端提交后拿到一个task_id之后靠轮询或者 SSE 拿进度服务端不阻塞。状态触发条件失败回滚到说明queued收到提交请求—写入任务存储等待 worker 领取preprocessingworker 领取任务failed音频重采样、视频抽帧、人脸检测tts_running前处理通过failed音色嵌入 语音合成avatar_runningwav 生成完毕failed口型驱动逐批出帧postprocessing帧序列生成完毕failedffmpeg 封装、音画对齐校验done封装成功—返回视频地址和耗时统计状态机的价值在排查阶段特别明显。任务卡住时先看它停在哪一步preprocessing停住基本是素材有问题人脸太小、音频全是静音avatar_running停住八成是显存或批次配置postprocessing停住直接去看 ffmpeg 日志。少了很多“我也不知道哪出问题”的时间。3. 跑起来显存、权重、依赖三道坎的实际操作这份源码能不能跑通基本取决于三件事Python 和 CUDA 版本对不对得上、权重有没有放对目录、第一次推理有没有用足够短的素材验证链路。我见过最常见的翻车方式是环境装完直接丢一段三分钟的视频进去跑跑到一半显存爆了还以为是自己显卡不行。3.1 环境与依赖Python 3.10 CUDA 12.1 这套组合Python 版本别乱升。3.11、3.12 上部分推理库的预编译 wheel 还缺失会退回源码编译然后卡在 nvcc 上半天。3.10 是这套源码的舒适区。# 1. 建一个干净的 conda 环境避免和系统里的 torch 打架 conda create -n dhclone python3.10 -y conda activate dhclone # 2. 先装 torch注意 cu121 要和驱动对得上 pip install torch2.1.2 torchvision0.16.2 --index-url https://download.pytorch.org/whl/cu121 # 3. 再装项目依赖避免 requirements 里把 torch 覆盖成 CPU 版 pip install -r requirements.txt # 4. ffmpeg 必须单独装容器里也要装后续封装全靠它 sudo apt-get install -y ffmpeg ffmpeg -version | head -n 1参数不用改但顺序不能乱。第 2 步和第 3 步反过来requirements.txt里如果没有锁死 torch 版本pip 可能给你装一个 CPU 版然后运行时才报Torch not compiled with CUDA enabled非常容易误判成驱动问题。装完先验一下 GPU 是不是真的能用import torch print(torch.__version__, torch.version.cuda) print(torch.cuda.is_available(), torch.cuda.get_device_name(0)) print(torch.cuda.get_device_properties(0).total_memory / 1024**3, GB)四个输出分别对应torch 版本、编译时的 CUDA 版本、是否可用、显卡名和显存。显存那一行尤其重要后面批大小要不要压全看它。3.2 权重落位目录映射表与校验权重是第二道坎。这套源码把models/留空靠脚本或手动放置必须放对位置代码里是按固定相对路径找的。权重类型目标路径用途校验方式TTS 主模型models/tts/main.pth语音合成主干文件大小与官方记录一致音色编码器models/tts/speaker.pt提取音色嵌入能被torch.load正常加载声码器models/tts/vocoder.pth波形重建采样率与配置一致人脸检测模型models/talking-head/face/对齐与裁剪目录下有 onnx 或 pth 文件口型驱动权重models/talking-head/generator.pth帧生成参数量与配置文件匹配常见做法是写一个scripts/download_weights.sh把文件列表和 sha256 一起维护。权重放错位置时报错往往很隐晦比如KeyError: model或者干脆加载了空字典跑出噪点视频少了个校验步骤就得排查很久。# 权重校验把预期 sha256 写进列表一次比对 sha256sum models/tts/*.pth models/talking-head/*.pth | tee /tmp/w.sha diff (sort /tmp/w.sha) (sort scripts/weights.sha256) echo 权重 OK3.3 启动与最小验证先跑 5 秒素材服务启动这块configs/server.yaml里主要是端口、并发数和临时目录。首次启动建议把并发压到 1worker 也只要 1 个方便看日志。# configs/server.yaml server: host: 0.0.0.0 port: 8000 max_concurrency: 1 # 首次启动压到 1稳定后再加 tmp_dir: /data/dhclone/tmp pipeline: stage_unload: true # 分阶段加载跑完一个阶段就卸载模型 keep_alive: falsestage_unload: true是个救命的开关。打开后语音模型跑完会主动释放显存再加载形象模型整体耗时变长但显存峰值能砍掉将近一半12 GB 卡全靠它。启动uvicorn app.api.main:app --host 0.0.0.0 --port 8000 --workers 1然后用最短的素材做一次端到端验证别直接上长视频# 5 秒参考音频 5 秒参考视频先确认链路通不通 curl -X POST http://127.0.0.1:8000/api/task \ -F text这是一次链路验证。 \ -F audio_refsamples/ref_5s.wav \ -F video_refsamples/face_5s.mp4 \ -F avatartrue返回task_id后用/api/task/{id}轮询状态。第一次跑能到done说明环境、权重、依赖全对再去试长素材。这个“先短后长”的顺序是我踩过坑之后养成的习惯——反过来做出问题时你分不清是环境问题还是素材问题。4. 核心代码从参考音频到口型视频的关键几步代码这块挑四个真正决定成片质量的位置讲音频前处理、音色嵌入与合成、人脸对齐与口型驱动、封装。其他部分路由、存储看一遍就懂不多赘述。4.1 参考音频预处理切片、降噪、响度归一很多“克隆出来不像”的问题根子在参考音频上不在模型上。手机录音的音量差异、首尾静音、背景底噪都会让音色嵌入偏移。# app/preprocess/audio.py import librosa import numpy as np TARGET_SR 16000 # TTS 主干统一吃 16k MIN_DUR, MAX_DUR 3.0, 10.0 # 单段参考音频的有效时长区间 def load_and_normalize(path: str) - np.ndarray: # 统一重采样和单声道避免多格式素材进模型时行为不一致 wav, _ librosa.load(path, srTARGET_SR, monoTrue) # 掐掉首尾长静音否则音色嵌入会被空白段稀释 wav, _ librosa.effects.trim(wav, top_db30) # 峰值归一化到约 -1 dBFS抹平不同设备录音的响度差异 peak np.max(np.abs(wav)) or 1.0 return wav / peak * 0.891 def split_reference(wav: np.ndarray): 把长参考切成 3~10 秒的若干段返回有效段列表 win int(MAX_DUR * TARGET_SR) hop int(MIN_DUR * TARGET_SR) chunks [] for start in range(0, max(len(wav) - hop, 1), hop): seg wav[start:start win] if len(seg) MIN_DUR * TARGET_SR: continue # 能量过低的段直接丢静音段对音色毫无贡献 if np.sqrt(np.mean(seg ** 2)) 0.01: continue chunks.append(seg) return chunkstop_db30是静音裁剪阈值越小裁得越狠人声弱的素材可以调到 25 左右。0.891是 -1 dBFS 的线性幅度别改成 1.0满幅容易在声码器重建时削波。split_reference里的MIN_DUR和MAX_DUR是这套源码里我调得最多的两个参数。3 秒以下音色嵌不稳10 秒以上模型会把多种语气平均掉出来的人声会变得没情绪。要是素材里说话人语气变化大宁可切成更多短段也不要一段硬塞。4.2 音色嵌入提取与 TTS 推理音色嵌入speaker embedding是整条语音链路的锚点提取一次就能复用。我在实际改造里会把它缓存下来同一个说话人第二次出片能省掉几秒。# app/inference/tts_engine.py import torch class TTSEngine: def __init__(self, cfg): self.device cfg[device] # cuda 或 cpu self.sr cfg[sample_rate] # 与训练时一致通常 32000 self.speed cfg.get(speed, 1.0) # 语速倍率1.0 为原速 self.main self._load(cfg[main_ckpt]) # 主干 self.spk self._load(cfg[speaker_ckpt]) # 音色编码器 self.vocoder self._load(cfg[vocoder_ckpt]) torch.inference_mode() def extract_speaker(self, wav: torch.Tensor) - torch.Tensor: # 输入形状 [1, T]输出 [1, D] 的音色向量 return self.spk(wav.unsqueeze(0).to(self.device)) torch.inference_mode() def synth(self, text: str, spk_emb: torch.Tensor, speed: float | None None) - torch.Tensor: # 文本先过正则化再进主干中文数字和多音字都在这一步处理 phones self.text_to_phones(text) mel self.main.infer( phones, spk_emb, speedspeed or self.speed, temperature0.6, # 越低越稳越高越有情绪但可能跑调 ) return self.vocoder(mel) # 输出 [1, T] 波形三个参数值得单独说。temperature控制语调随机性做口播视频建议 0.50.7做带情绪的短视频可以拉到 0.8但再高就开始出现吞字。speed是全局语速别指望用它来对嘴型——嘴型对齐是形象链路的事语速改得太夸张反而让口型模型判断错。sample_rate必须和训练时一致改成 16k 输出再重采样到 32k音质会明显发闷。text_to_phones这一步不是可有可无的工程细节。中文里的“2024”“3.5%”“A股”不做正则化直接进模型出来是逐字符念的非常生硬。4.3 人脸对齐与口型驱动批次和时序是两回事形象链路的输入是参考视频抽出来的帧序列加上 4.2 产出的 wav。核心是先把人脸框稳定住再逐批喂给生成模型。# app/inference/avatar_engine.py import cv2 import numpy as np class AvatarEngine: def __init__(self, cfg): self.det self._build_detector(cfg[face_model_dir]) self.gen self._load_generator(cfg[generator_ckpt]) self.batch cfg.get(batch_size, 8) # 扩散模型 8 帧一批比较稳 self.fps cfg.get(fps, 25) # 必须和封装时的帧率一致 def align(self, frame: np.ndarray, ref_boxNone): faces self.det(frame) if not faces: # 检测不到就沿用上一帧的框直接报错会让整段视频报废 if ref_box is None: raise ValueError(首帧未检测到人脸) return ref_box # 取面积最大的脸多人的素材里主角通常是最近的那个 box max(faces, keylambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])).bbox return self._expand(box, ratio0.35) # 外扩留出下巴和额头 def drive(self, frames, audio_feat): out [] for i in range(0, len(frames), self.batch): chunk frames[i:i self.batch] feat audio_feat[i:i self.batch] # 音频特征按帧对齐切 out.extend(self.gen(chunk, feat)) return outbatch_size8是个平衡点。调到 16 出片更快但显存会多占 40% 左右24 GB 卡才建议动。ratio0.35的框外扩很关键扩得太小下巴被裁掉模型只能瞎猜嘴型出来会有明显的糊边扩得太大背景占比高脸在画面里变小清晰度下降。audio_feat的切片必须和视频帧严格一一对应这里最容易出错——音频特征的时间分辨率通常比视频帧高得先重采样到fps再切否则整段视频的嘴型会系统性地偏前或偏后。4.4 合成封装帧序列 音频 mux 的三个必设参数最后一步是把 PNG 帧序列和 wav 封成 mp4。这条命令看着简单参数错一个就是浏览器播不了、或者音画对不上。# app/postprocess/mux.py import subprocess def mux(frames_dir: str, fps: int, audio_path: str, out_path: str): cmd [ ffmpeg, -y, -framerate, str(fps), # 必须等于生成时的 fps -i, f{frames_dir}/%06d.png, -i, audio_path, -c:v, libx264, -pix_fmt, yuv420p, # 不加这行部分浏览器黑屏 -crf, 18, # 画质18 接近视觉无损 -c:a, aac, -b:a, 192k, -shortest, # 音画取较短者防止尾部拖帧 out_path, ] subprocess.run(cmd, checkTrue, capture_outputTrue)-framerate要放在-i前面写成-r放后面语义就变成输出帧率会触发重采样帧数不变但时间轴被拉伸出来就是音画不同步。-pix_fmt yuv420p是为了兼容不加的话部分播放器和浏览器直接黑屏这个坑我掉过不止一次。-crf 18是画质和体积的平衡点做口播视频 1820 足够。封装完别急着返回加一步校验# 校验封装结果帧率和时长都要看一眼 ffprobe -v error -select_streams v:0 -show_entries streamr_frame_rate,nb_frames \ -show_entries formatduration -of defaultnw1 out.mp4输出的r_frame_rate应该是25/1duration和音频时长差值控制在 0.1 秒内超了就是帧对齐那步出的问题。5. 避坑排查显存、音画、人脸、文本四类高频故障下面这几条都是这套源码实际跑起来之后最容易碰上的按“现象 → 原因 → 解决”列清楚遇到时可以直接对号入座。5.1 现象跑到 60% 左右报 CUDA out of memory原因语音模型和形象模型同时常驻显存。默认配置下两个引擎在服务启动时就都加载了语音跑完不释放等形象模型开始跑峰值直接顶到天花板。解决把configs/server.yaml里的stage_unload打开让流水线按阶段加载和卸载同时在pipeline.py里任务结束主动清一次缓存# app/core/pipeline.py import gc, torch def release(): gc.collect() torch.cuda.empty_cache() torch.cuda.ipc_collect()还有一点容易被忽略batch_size是按 24 GB 卡调的默认值12 GB 卡要手动压到 4 甚至 2别指望模型自己降。5.2 现象成品里声音比嘴型快半秒到一秒原因三个地方都会导致。抽帧fps和封装-framerate不一致音频特征重采样到视频帧率时用了向下取整-shortest之外还额外加了-t截断。解决把fps抽成一个全局常量前处理、推理、封装三处都从同一个配置读禁止各写各的。封装前先算一遍帧数 / fps和音频时长的差值超过 0.05 秒就报警而不是继续跑。做二次开发时我给这一步加了个断言音画不同步从此没再出现过。5.3 现象人脸边缘抖动、背景闪烁同一个视频两次跑结果还不一样原因逐帧独立推理帧与帧之间没有时序约束模型自己也不知道上一帧长什么样。如果生成过程里还有随机采样扩散模型的默认行为同一输入两次输出必然不同。解决固定随机种子并且在帧间做指数滑动平均EMA平滑关键点# 关键点帧间平滑alpha 越小越稳但会略显迟钝 alpha 0.6 smooth alpha * prev (1 - alpha) * curalpha建议 0.50.7。低于 0.5 会出现“脸部跟不动”的滞后感高于 0.8 基本等于没平滑。另外参考视频尽量选机位固定、光线均匀的手持晃动的素材再怎么平滑也救不回来。5.4 现象中文里的数字、英文单词、单位符号念得乱七八糟原因文本正则化TN缺失模型按字符逐个念。2024被念成“二零二四”还算好的3.5%直接变成“三点五百分号”就废了。解决在text_to_phones前面插一层 TN数字转中文读法、单位转全称、英文缩写按字母或音译处理再挂一个多音字词典。这一层不用做得多复杂覆盖日期、金额、百分比、常见单位四类日常口播素材基本够用。改完记得拿一段包含数字和英文的文案回归一遍别改完中文修好了英文又坏了。5.5 现象换一台机器就跑不起来或者输出视频绿屏原因CUDA 架构不匹配、ffmpeg 版本差异、pix_fmt没设。这三件事在两台机器上表现各不相同但根子是环境没锁死。解决用项目里的docker/把镜像固定下来基础镜像的 CUDA 版本、ffmpeg 版本、torch 版本三件套全部钉死不要在宿主机裸跑。绿屏问题九成是-pix_fmt yuv420p漏了剩下一成是帧序列里混了 RGBA 的 PNG。6. 二次开发把克隆能力接进业务流的四个改造点这套源码拿来直接用是可以的但真要接到业务里我一般会动四个地方。第一个是把参数全部外置到请求体别让人为了改个语速去重启服务第二个是加音色缓存同一个说话人的嵌入算一次存起来这是投入产出比最高的一处优化第三个是把长任务拆成可中断的子步骤用户取消时能真正停掉 GPU 占用第四个是加一层 adapter把内部任务格式和业务侧的字段解耦。改造点改动位置难度收益参数请求体化app/api/*_router.py低免重启调参音色嵌入缓存app/inference/tts_engine.py低单任务省 38 秒任务可中断app/core/pipeline.py中避免 GPU 被占死adapter 层新增app/adapters/中业务字段解耦音色缓存写起来最简单收益也最直接按参考音频的哈希做 key# app/inference/cache.py import hashlib, torch, os def audio_key(wav_bytes: bytes) - str: # 用音频内容哈希避免同一段素材换文件名后重复计算 return hashlib.sha1(wav_bytes).hexdigest() def get_or_extract(engine, key: str, wav: torch.Tensor, cache_dir.cache/spk): os.makedirs(cache_dir, exist_okTrue) path os.path.join(cache_dir, f{key}.pt) if os.path.exists(path): return torch.load(path, map_locationengine.device) emb engine.extract_speaker(wav) torch.save(emb, path) return emb改造完怎么验证别凭感觉。语音侧拿同一段参考音频合成同一句话比对两次输出的梅尔频谱距离改动前后差值应该在噪声范围内形象侧看口型同步的客观指标LSE-C / LSE-D 这类别只盯着肉眼觉得“差不多”。我一般会准备一组固定的回归素材一段中文带数字、一段中英混说、一段长句、一张正脸视频、一张侧脸偏多的视频每次改完跑一遍出问题能立刻定位是哪条链路退化的。还有个血泪经验改pipeline.py之前一定先把当前能跑通的版本打一个 tag。任务调度这块是整个项目里最难测的部分状态回调、异常捕获、资源释放纠缠在一起改坏一次排查半天。从那以后我每次动核心链路前都强制走一遍小组回归不图快。这套源码的价值不在于模型多新而在于它把数字人克隆这条又长又碎的链路拆成了能单独替换的零件语音模型想换就换形象模型想换也只需要动avatar_engine。希望你也能顺着这个结构把它改成自己业务里那个能稳定出片的版本。本文还有配套的精品资源点击获取