7.3 KiB
7.3 KiB
Qwen3-TTS 声音克隆集成到 ViGent2
需求概述
- 前端支持上传/在线录制参考音频(wav, mp3, m4a 等)
- EdgeTTS 音色保留,增加 Qwen3-TTS 声音克隆界面
- 两种 TTS 方式做成统一界面(Tab 切换)
- 声音克隆使用相同的口播文案输入
架构设计
GPU 分配
| GPU | 服务 | 模型 |
|---|---|---|
| GPU0 | Qwen3-TTS | 0.6B-Base (声音克隆) |
| GPU1 | LatentSync | 1.6 (唇形同步) |
存储
- 新增 Supabase bucket:
ref_audios - 路径格式:
{user_id}/{timestamp}_{filename}.wav
实现步骤
1. 后端:新建声音克隆服务
文件: backend/app/services/voice_clone_service.py
class VoiceCloneService:
def __init__(self):
self.gpu_id = 0
self.model_path = "models/Qwen3-TTS/checkpoints/0.6B-Base"
self._model = None
self._lock = asyncio.Lock()
async def generate_audio(self, text, ref_audio_path, ref_text, output_path, language="Chinese"):
# 使用 Qwen3TTSModel.generate_voice_clone()
2. 后端:新建参考音频 API
文件: backend/app/api/ref_audios.py
| 接口 | 方法 | 功能 |
|---|---|---|
/api/ref-audios |
POST | 上传参考音频 + ref_text |
/api/ref-audios |
GET | 列出用户的参考音频 |
/api/ref-audios/{id} |
DELETE | 删除参考音频 |
上传时自动转换为 wav (16kHz mono),存储 ref_text 元数据。
3. 后端:修改视频生成 API
文件: backend/app/api/videos.py
扩展 GenerateRequest:
class GenerateRequest(BaseModel):
text: str
voice: str = "zh-CN-YunxiNeural"
material_path: str
# 新增
tts_mode: str = "edgetts" # "edgetts" | "voiceclone"
ref_audio_id: Optional[str] = None
ref_text: Optional[str] = None
修改 _process_video_generation():
if req.tts_mode == "voiceclone":
await voice_clone_service.generate_audio(...)
else:
await tts_service.generate_audio(...)
4. 后端:注册路由
文件: backend/app/main.py
from app.api import ref_audios
app.include_router(ref_audios.router, prefix="/api/ref-audios", tags=["ref-audios"])
5. 前端:改造音色选择区域
文件: frontend/src/app/page.tsx
新增状态:
const [ttsMode, setTtsMode] = useState<'edgetts' | 'voiceclone'>('edgetts');
const [refAudios, setRefAudios] = useState<RefAudio[]>([]);
const [selectedRefAudio, setSelectedRefAudio] = useState<RefAudio | null>(null);
const [refText, setRefText] = useState('');
// 在线录音相关
const [isRecording, setIsRecording] = useState(false);
const [recordedBlob, setRecordedBlob] = useState<Blob | null>(null);
const [recordingTime, setRecordingTime] = useState(0);
const mediaRecorderRef = useRef<MediaRecorder | null>(null);
UI 结构:
┌─────────────────────────────────────┐
│ 🎙️ 选择配音方式 │
├─────────────────────────────────────┤
│ [EdgeTTS 音色] [声音克隆] ← Tab │
├─────────────────────────────────────┤
│ Tab 1: 现有音色 2x3 网格 │
│ │
│ Tab 2: 声音克隆 │
│ ┌───────────────────────────────┐ │
│ │ 📁 我的参考音频 │ │
│ │ [ref1] [ref2] [+上传] │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ 🎤 或在线录音 │ │
│ │ [开始录音] [停止] 时长: 0:05 │ │
│ │ (录音完成后显示试听和使用按钮) │ │
│ └───────────────────────────────┘ │
│ ┌───────────────────────────────┐ │
│ │ 📝 参考音频文字 (必填) │ │
│ │ [textarea] │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
在线录音逻辑:
const startRecording = async () => {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm' });
const chunks: BlobPart[] = [];
mediaRecorder.ondataavailable = (e) => chunks.push(e.data);
mediaRecorder.onstop = () => {
const blob = new Blob(chunks, { type: 'audio/webm' });
setRecordedBlob(blob);
stream.getTracks().forEach(track => track.stop());
};
mediaRecorder.start();
setIsRecording(true);
mediaRecorderRef.current = mediaRecorder;
};
const stopRecording = () => {
mediaRecorderRef.current?.stop();
setIsRecording(false);
};
const useRecording = async () => {
// 将录音 Blob 上传到后端
const formData = new FormData();
formData.append('file', recordedBlob, 'recording.webm');
formData.append('ref_text', refText);
const { data } = await api.post('/api/ref-audios', formData);
// 上传成功后刷新列表并选中
fetchRefAudios();
setSelectedRefAudio(data);
};
6. 前端:修改生成请求
const handleGenerate = async () => {
const payload = {
material_path: materialObj.path,
text: text,
tts_mode: ttsMode,
...(ttsMode === 'edgetts'
? { voice }
: { ref_audio_id: selectedRefAudio.id, ref_text: refText })
};
await api.post('/api/videos/generate', payload);
};
文件清单
新建
| 文件 | 描述 |
|---|---|
backend/app/services/voice_clone_service.py |
声音克隆服务 |
backend/app/api/ref_audios.py |
参考音频管理 API |
修改
| 文件 | 修改内容 |
|---|---|
backend/app/api/videos.py |
扩展 GenerateRequest,修改 TTS 调用逻辑 |
backend/app/main.py |
注册 ref_audios 路由 |
backend/app/services/storage.py |
添加 BUCKET_REF_AUDIOS |
frontend/src/app/page.tsx |
Tab 切换 UI、参考音频选择、refText 输入 |
验证方法
-
后端测试:
# 启动后端 cd backend && uvicorn app.main:app --port 8006 # 测试参考音频上传 curl -X POST http://localhost:8006/api/ref-audios \ -F "file=@test.wav" -F "ref_text=测试文字" # 测试声音克隆生成 curl -X POST http://localhost:8006/api/videos/generate \ -H "Content-Type: application/json" \ -d '{"text":"测试文案","tts_mode":"voiceclone","ref_audio_id":"xxx","ref_text":"参考文字","material_path":"..."}' -
前端测试:
- 打开首页,确认 Tab 切换正常
- 上传参考音频,确认列表显示
- 选择声音克隆模式,填写参考文字,点击生成
- 确认生成的视频使用克隆的声音
-
端到端测试:
- 上传参考音频 → 选择声音克隆 → 输入口播文案 → 生成视频 → 播放验证声音