一、引言

近年来,随着生成式人工智能的爆发,语音克隆技术逐渐从实验室走向实际应用场景,如个性化语音助手、有声内容创作、虚拟数字人等。传统的语音克隆方案往往依赖大规模GPU集群和高昂的云端API费用,对中小企业或个人开发者并不友好。与此同时,国产算力平台正在快速崛起,以海光DCU(深度计算单元)为代表的异构加速卡,凭借其兼容CUDA生态和较高的性价比,为AI应用的本地化部署提供了新的可能。

本文基于海光DCU K00_AI单卡环境,设计并实现了一套完整的语音克隆Web系统。该系统采用B/S架构,前端提供音频上传、录音、参考文本修正和目标文本合成等交互功能;后端集成自动语音识别(ASR)和语音合成(TTS)服务,其中TTS选用Qwen3-TTS-12Hz-1.7B-Base模型,支持基于参考音频的零样本语音克隆。系统还包含音频格式转换(ffmpeg)、临时文件自动清理、Base64音频编码传输等工程化细节,确保在单卡资源受限的条件下,仍能稳定处理并发请求。

整个项目从实际部署角度出发,解决了三个核心问题:一是如何将用户提供的任意音频(含WebM录音)转换为标准WAV格式以适配ASR接口;二是如何利用ASR结果辅助参考文本的准确提取,提升克隆质量;三是如何通过RESTful API将TTS生成的音频流直接返回前端播放与下载。通过该系统的开源实践,我们希望为国产AI硬件生态贡献一份可复用的参考案例,同时也为语音技术爱好者提供一个快速上手的脚手架。在后续章节中,我们将详细阐述系统架构设计、模块划分、关键代码逻辑以及运行效果,并分享在海光DCU上部署时的性能调优经验。

二、方案设计

1总体架构

系统采用前后端分离的Flask Web应用,整体分为三层:用户交互层、业务逻辑层和模型服务层。用户交互层提供HTML页面,支持音频文件上传、浏览器录音(MediaRecorder)、参考文本编辑和合成文本输入。业务逻辑层由Flask路由构成,负责音频格式转换、ASR请求封装、TTS请求构造以及临时文件生命周期管理。模型服务层则独立部署ASR服务(端口8084)和TTS服务(端口8088),两者均基于海光DCU加速推理。

2核心模块

音频预处理模块:利用ffmpeg将上传的非WAV格式(如WebM)转为16kHz、16bit PCM的WAV文件,确保ASR接口的兼容性。

ASR接入模块:读取本地WAV文件,以multipart/form-data形式调用ASR接口,并从返回文本中通过正则提取<asr_text>标签内的内容,作为参考文本。

语音克隆模块:将参考WAV编码为Base64 DataURI,与参考文本、目标文本一同构造成JSON请求,调用TTS服务的/v1/audio/speech端点,获得合成音频二进制流并保存为WAV文件,返回可访问的URL。

文件管理模块:设置临时文件夹(temp_audio和temp_synth),并启动后台守护线程每10分钟清理一次超过30分钟的文件,防止磁盘占满。

前端交互模块:利用HTML5原生API实现录音、文件选择和音频播放,通过Fetch异步请求与后端通信,提升用户体验。

3技术选型与部署

Web框架:Flask(轻量,适合快速原型)。

ASR/TTS服务:均采用HTTP接口,便于解耦,可独立扩展。

硬件平台:海光DCU K00_AI单卡(显存32GB),通过torch和transformers加载Qwen3-TTS模型,实测单次克隆请求响应时间在3~5秒内(含推理)。

并发策略:单卡下使用同步处理,配合Nginx+uWSGI可提升并发能力,但本方案侧重单用户场景。

程序工作流程图

、实施方法及代码

3.1硬件环境

本方案的硬件平台为一台H3C服务器,配置如下:

组件

规格

CPU

2×海光74902.7GHz64C

内存

16×32GDDR5

GPU

8×海光DCU64GBK100_AI

需占用一张K100_AI显卡

3.2软件栈

本方案的软件栈基于Docker容器化技术构建,使用经过海光DCU适配的vLLM推理镜像:

镜像一(Qwen3-ASR-1.7B):
harbor.sourcefind.cn:5443/dcu/admin/base/custom:vllm0.15.1-ubuntu22.04-dtk26.04-0130-py3.10-20260220

该镜像基于vLLM 0.15.1推理框架、DTK 26.04,Python 3.10环境,单卡部署Qwen3-ASR-1.7B,200ms即可完成语音识别。

镜像Qwen3-TTS-12Hz-1.7B-Base):

harbor.sourcefind.cn:5443/dcu/admin/base/custom:vllm018-ubuntu22.04-dtk26.04-nemotron-20260422

该镜像基于vLLM0.18.1和vLLM-omni 0.18.0推理框架、DTK 26.04,Python 3.10环境,单卡部署Qwen3-TTS-12Hz-1.7B-Base,平均语音合成延迟低于97ms。

软件项目:

1、Qwen3-ASR-1.7B环境部署参照https://developer.sourcefind.cn/codes/modelzoo/qwen3-asr_pytorch里面操作步骤,项目下载链接为:https://developer.sourcefind.cn/codes/modelzoo/qwen3-asr_pytorch/-/archive/main/qwen3-asr_pytorch-main.zip。

2、Qwen3-TTS-12Hz-1.7B-Base环境部署参照https://developer.sourcefind.cn/codes/modelzoo/qwen3-tts_pytorch里面操作步骤,项目下载链接为:https://developer.sourcefind.cn/codes/modelzoo/qwen3-tts_pytorch/-/archive/master/qwen3-tts_pytorch-master.zip。

3、语音克隆系统自编程序代码如下:

import os
import re
import uuid
import time
import base64
import threading
from flask import Flask, request, jsonify, render_template_string, send_from_directory
import requests

app = Flask(__name__)

# ---------- 配置 ----------
ASR_URL = "http://192.168.222.65:8084/v1/audio/transcriptions"
TTS_URL = "http://192.168.222.65:8088/v1/audio/speech"
AUTH_HEADER = {"Authorization": "Bearer PassWord@123456"}
UPLOAD_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_audio")
SYNTH_FOLDER = os.path.join(os.path.dirname(os.path.abspath(__file__)), "temp_synth")
os.makedirs(UPLOAD_FOLDER, exist_ok=True)
os.makedirs(SYNTH_FOLDER, exist_ok=True)

# ---------- 临时文件定期清理 ----------
def cleanup_old_files():
    """删除超过30分钟的临时文件"""
    now = time.time()
    for folder in [UPLOAD_FOLDER, SYNTH_FOLDER]:
        for fname in os.listdir(folder):
            fpath = os.path.join(folder, fname)
            if os.path.isfile(fpath) and now - os.path.getmtime(fpath) > 1800:
                try:
                    os.remove(fpath)
                except Exception:
                    pass

def periodic_cleanup():
    while True:
        cleanup_old_files()
        time.sleep(600)  # 每10分钟清理一次

# 启动清理线程
threading.Thread(target=periodic_cleanup, daemon=True).start()

# ---------- 前端页面 ----------
HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>语音克隆系统</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 40px auto; padding: 0 20px; }
        .section { margin-bottom: 30px; }
        label { font-weight: bold; display: block; margin-top: 10px; }
        textarea, input[type="text"] { width: 100%; padding: 8px; margin-top: 5px; box-sizing: border-box; }
        button { padding: 10px 20px; margin-right: 10px; cursor: pointer; }
        audio { width: 100%; margin-top: 10px; }
        .hidden { display: none; }
        .error { color: red; }
        .info { color: #555; font-size: 0.9em; }
    </style>
</head>
<body>
    <h1>🔊 语音克隆系统</h1>

    <!-- 步骤1:上传/录音 -->
    <div class="section">
        <h3>第一步:上传或录制参考音频</h3>
        <input type="file" id="audioFile" accept="audio/*">
        <button id="uploadBtn">上传并识别</button>
        <span class="info">或</span>
        <button id="startRecord">开始录音</button>
        <button id="stopRecord" disabled>停止录音</button>
        <div id="recordingStatus" class="info"></div>
    </div>

    <!-- 步骤2:参考文本(ASR识别结果,可修改) -->
    <div class="section">
        <h3>第二步:参考文本(音频内容,可修改)</h3>
        <textarea id="refText" rows="3" placeholder="ASR 识别结果会显示在这里..."></textarea>
        <input type="hidden" id="fileId">
    </div>

    <!-- 步骤3:目标合成文本 -->
    <div class="section">
        <h3>第三步:要合成的文本</h3>
        <textarea id="synthText" rows="4" placeholder="请输入您想要合成的文本内容...">滚滚长江东逝水,浪花淘尽英雄。是非成败转头空。青山依旧在,几度夕阳红。白发渔樵江渚上,惯看秋月春风。一壶浊酒喜相逢。古今多少事,都付笑谈中。</textarea>
    </div>

    <!-- 步骤4:克隆并播放 -->
    <div class="section">
        <button id="cloneBtn">开始克隆</button>
        <span id="cloneStatus" class="info"></span>
        <div id="audioPlayer" class="hidden">
            <h3>合成结果</h3>
            <audio controls id="audioEl"></audio>
            <br>
            <a id="downloadLink" href="#" download="cloned_audio.wav">下载音频</a>
        </div>
    </div>

    <div id="errorMsg" class="error"></div>

    <script>
        let mediaRecorder;
        let audioChunks = [];
        let recordedBlob = null;

        // 录音功能
        document.getElementById('startRecord').onclick = async () => {
            audioChunks = [];
            recordedBlob = null;
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                mediaRecorder = new MediaRecorder(stream);
                mediaRecorder.ondataavailable = e => audioChunks.push(e.data);
                mediaRecorder.onstop = () => {
                    recordedBlob = new Blob(audioChunks, { type: 'audio/webm' });
                    document.getElementById('recordingStatus').textContent = '录音完成,准备上传';
                };
                mediaRecorder.start();
                document.getElementById('startRecord').disabled = true;
                document.getElementById('stopRecord').disabled = false;
                document.getElementById('recordingStatus').textContent = '正在录音...';
            } catch(err) {
                showError('无法访问麦克风: ' + err.message);
            }
        };

        document.getElementById('stopRecord').onclick = () => {
            if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                mediaRecorder.stop();
                mediaRecorder.stream.getTracks().forEach(t => t.stop());
                document.getElementById('startRecord').disabled = false;
                document.getElementById('stopRecord').disabled = true;
            }
        };

        // 上传音频并识别
        function uploadAudio(blob) {
            const formData = new FormData();
            const file = document.getElementById('audioFile').files[0];
            if (file) {
                formData.append('file', file);
            } else if (blob) {
                // 浏览器录音默认是 webm,服务端需要转成 wav(简单处理:后端调用 ffmpeg?这里使用前端编码 wav)
                // 为了简化,将录音 blob 直接发送,后端用 ffmpeg 转码(需安装 ffmpeg)
                formData.append('file', blob, 'recording.webm');
            } else {
                showError('请先选择音频文件或完成录音');
                return;
            }

            document.getElementById('uploadBtn').disabled = true;
            fetch('/upload', { method: 'POST', body: formData })
                .then(r => r.json().then(data => ({ status: r.status, body: data })))
                .then(({ status, body }) => {
                    if (status === 200) {
                        document.getElementById('refText').value = body.text || '';
                        document.getElementById('fileId').value = body.file_id || '';
                        document.getElementById('errorMsg').textContent = '';
                    } else {
                        showError(body.error || '识别失败');
                    }
                })
                .catch(err => showError('请求出错: ' + err.message))
                .finally(() => { document.getElementById('uploadBtn').disabled = false; });
        }

        // 上传按钮(文件选择)
        document.getElementById('uploadBtn').onclick = () => {
            uploadAudio(null);
        };

        // 录音停止后自动上传(可选,这里提供一个自动上传机制)
        document.getElementById('stopRecord').addEventListener('click', function() {
            // 延迟一下等待 blob 准备就绪
            setTimeout(() => {
                if (recordedBlob) {
                    uploadAudio(recordedBlob);
                }
            }, 200);
        });

        // 克隆按钮
        document.getElementById('cloneBtn').onclick = () => {
            const fileId = document.getElementById('fileId').value;
            const refText = document.getElementById('refText').value.trim();
            const synthText = document.getElementById('synthText').value.trim();
            if (!fileId) { showError('请先上传并识别参考音频'); return; }
            if (!refText) { showError('参考文本不能为空'); return; }
            if (!synthText) { showError('合成文本不能为空'); return; }

            document.getElementById('cloneBtn').disabled = true;
            document.getElementById('cloneStatus').textContent = '正在合成...';

            fetch('/clone', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    file_id: fileId,
                    ref_text: refText,
                    input_text: synthText
                })
            })
            .then(r => r.json().then(data => ({ status: r.status, body: data })))
            .then(({ status, body }) => {
                if (status === 200 && body.url) {
                    const audioEl = document.getElementById('audioEl');
                    audioEl.src = body.url;
                    document.getElementById('audioPlayer').classList.remove('hidden');
                    document.getElementById('downloadLink').href = body.url;
                    document.getElementById('cloneStatus').textContent = '合成完成';
                    document.getElementById('errorMsg').textContent = '';
                } else {
                    showError(body.error || '克隆失败');
                }
            })
            .catch(err => showError('请求出错: ' + err.message))
            .finally(() => {
                document.getElementById('cloneBtn').disabled = false;
                document.getElementById('cloneStatus').textContent = '';
            });
        };

        function showError(msg) {
            document.getElementById('errorMsg').textContent = msg;
        }
    </script>
</body>
</html>
"""

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

# ---------- ASR 识别接口 ----------
@app.route('/upload', methods=['POST'])
def upload_and_recognize():
    if 'file' not in request.files:
        return jsonify({'error': '没有上传文件'}), 400

    file = request.files['file']
    if file.filename == '':
        return jsonify({'error': '文件名为空'}), 400

    # 保存原始上传文件(可能为 webm,需要转成 wav 再发给 ASR)
    original_name = file.filename
    ext = os.path.splitext(original_name)[1].lower()
    file_id = str(uuid.uuid4())
    temp_original_path = os.path.join(UPLOAD_FOLDER, f"{file_id}_original{ext}")
    file.save(temp_original_path)

    # 如果不是 wav,尝试用 ffmpeg 转换为 wav(需安装 ffmpeg)
    wav_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.wav")
    if ext != '.wav':
        import subprocess
        try:
            subprocess.run(
                ['ffmpeg', '-y', '-i', temp_original_path, '-acodec', 'pcm_s16le', '-ar', '16000', wav_path],
                check=True, capture_output=True, timeout=30
            )
            os.remove(temp_original_path)  # 删除原始非 wav 文件
        except Exception as e:
            return jsonify({'error': f'音频转换失败(需要 ffmpeg 支持): {str(e)}'}), 500
    else:
        # 已经是 wav,直接重命名
        os.rename(temp_original_path, wav_path)

    # 调用 ASR API
    try:
        with open(wav_path, 'rb') as f:
            files = {'file': (os.path.basename(wav_path), f, 'audio/wav')}
            resp = requests.post(ASR_URL, headers=AUTH_HEADER, files=files, timeout=60)
        if resp.status_code != 200:
            return jsonify({'error': f'ASR 服务返回错误: {resp.text}'}), 502
        result = resp.json()
        raw_text = result.get('text', '')
        # 解析 "<asr_text>..." 格式
        match = re.search(r'<asr_text>(.*)', raw_text, re.DOTALL)
        if match:
            recognized_text = match.group(1).strip()
        else:
            recognized_text = raw_text.strip()
        # 保留 wav 文件用于后续克隆
        # 清理旧文件已在后台线程处理
        return jsonify({'text': recognized_text, 'file_id': file_id})
    except Exception as e:
        return jsonify({'error': f'ASR 请求异常: {str(e)}'}), 500

# ---------- 语音克隆接口 ----------
@app.route('/clone', methods=['POST'])
def clone_voice():
    data = request.get_json()
    if not data:
        return jsonify({'error': '请求数据为空'}), 400
    file_id = data.get('file_id')
    ref_text = data.get('ref_text', '').strip()
    input_text = data.get('input_text', '').strip()
    if not file_id or not ref_text or not input_text:
        return jsonify({'error': '缺少必要参数 file_id, ref_text, input_text'}), 400

    # 查找原始 wav 文件
    wav_path = os.path.join(UPLOAD_FOLDER, f"{file_id}.wav")
    if not os.path.exists(wav_path):
        return jsonify({'error': '参考音频文件不存在或已过期,请重新上传'}), 404

    # 读取音频并编码为 base64
    try:
        with open(wav_path, 'rb') as f:
            audio_b64 = base64.b64encode(f.read()).decode('utf-8')
    except Exception as e:
        return jsonify({'error': f'读取音频文件失败: {str(e)}'}), 500

    ref_audio_data = f"data:audio/wav;base64,{audio_b64}"

    # 调用 TTS 克隆 API
    tts_payload = {
        "model": "Qwen3-TTS-12Hz-1.7B-Base",
        "input": input_text,
        "language": "Chinese",
        "task_type": "Base",
        "x_vector_only_mode": False,
        "ref_text": ref_text,
        "ref_audio": ref_audio_data,
        "max_new_tokens": 256
    }

    try:
        resp = requests.post(TTS_URL, json=tts_payload, headers=AUTH_HEADER, timeout=120)
        if resp.status_code != 200:
            return jsonify({'error': f'TTS 服务返回错误: {resp.text}'}), 502
        # 保存合成音频
        synth_id = str(uuid.uuid4())
        synth_path = os.path.join(SYNTH_FOLDER, f"{synth_id}.wav")
        with open(synth_path, 'wb') as f:
            f.write(resp.content)
        return jsonify({'url': f'/serve_audio/{synth_id}.wav'})
    except Exception as e:
        return jsonify({'error': f'TTS 请求异常: {str(e)}'}), 500

# ---------- 提供音频文件下载/播放 ----------
@app.route('/serve_audio/<filename>')
def serve_audio(filename):
    return send_from_directory(SYNTH_FOLDER, filename, as_attachment=False)

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=True)

3.3模型下载与准备

1、Qwen3-ASR-1.7B语音识别模型):

下载链接:https://modelscope.cn/models/Qwen/Qwen3-ASR-1.7B/files

2、Qwen3-TTS-12Hz-1.7B-Base(语音克隆模型)

下载链接:

https://modelscope.cn/models/Qwen/Qwen3-TTS-12Hz-1.7B-Base/files

3.4 模型启动参数

1、Qwen3-ASR-1.7B启动参数:

cat Qwen3-ASR-1.7B.sh 
HIP_VISIBLE_DEVICES=6 vllm serve '/home/models/Qwen3-ASR-1.7B' \
  --trust-remote-code \
  --gpu-memory-utilization 0.3 \
  --limit-mm-per-prompt '{"audio": 1}' \
  --port 8084 \
  --served-model-name Qwen3-ASR-1.7B \
  --api-key PassWord@123456

2、Qwen3-TTS-12Hz-1.7B-Base启动参数:

cat Qwen3-TTS-12Hz-1.7B-Base.sh
HIP_VISIBLE_DEVICES=6 vllm-omni serve /home/models/Qwen3-TTS-12Hz-1.7B-Base \
    --stage-configs-path qwen3-tts_pytorch-master/qwen3_tts.yaml \
    --gpu-memory-utilization 0.1 \
    --omni \
    --port 8088 \
    --trust-remote-code \
    --enforce-eager \
--served-model-name Qwen3-TTS-12Hz-1.7B-Base
cat qwen3-tts_pytorch-master/qwen3_tts.yaml 
async_chunk: true
stage_args:
  - stage_id: 0
    stage_type: llm
    is_comprehension: true
    runtime:
      devices: "0"
    engine_args:
      model_stage: qwen3_tts
      max_num_seqs: 10
      model_arch: Qwen3TTSTalkerForConditionalGeneration
      worker_type: ar
      scheduler_cls: vllm_omni.core.sched.omni_ar_scheduler.OmniARScheduler
      enforce_eager: false
      trust_remote_code: true
      async_scheduling: true
      enable_prefix_caching: false
      engine_output_type: latent
      gpu_memory_utilization: 0.1
      distributed_executor_backend: "mp"
      max_num_batched_tokens: 512
      max_model_len: 4096
      custom_process_next_stage_input_func: vllm_omni.model_executor.stage_input_processors.qwen3_tts.talker2code2wav_async_chunk
    # Use named connector to apply runtime.connectors.extra.
    output_connectors:
      to_stage_1: connector_of_shared_memory
    default_sampling_params:
      temperature: 0.9
      top_k: 50
      max_tokens: 4096
      seed: 42
      detokenize: false
      repetition_penalty: 1.05
      stop_token_ids: [2150]

  - stage_id: 1
    stage_type: llm
    runtime:
      devices: "0"
    engine_args:
      model_stage: code2wav
      max_num_seqs: 1
      model_arch: Qwen3TTSCode2Wav
      worker_type: generation
      scheduler_cls: vllm_omni.core.sched.omni_generation_scheduler.OmniGenerationScheduler
      enforce_eager: true
      trust_remote_code: true
      async_scheduling: true
      enable_prefix_caching: false
      engine_output_type: audio
      gpu_memory_utilization: 0.3
      distributed_executor_backend: "mp"
      # Must be divisible by num_code_groups and cover (left_context + chunk).
      max_num_batched_tokens: 8192
      # async_chunk appends windows per step; max_model_len must cover accumulated stream.
      max_model_len: 32768
    engine_input_source: [0]
    final_output: true
    final_output_type: audio
    # Distributed connector configuration
    input_connectors:
      from_stage_0: connector_of_shared_memory
    tts_args:
      max_instructions_length: 500
    default_sampling_params:
      temperature: 0.0
      top_p: 1.0
      top_k: -1
      max_tokens: 65536
      seed: 42
      detokenize: true
      repetition_penalty: 1.0

runtime:
  enabled: true
  defaults:
    window_size: -1
    max_inflight: 1

  connectors:
    connector_of_shared_memory:
      name: SharedMemoryConnector
      extra:
        shm_threshold_bytes: 65536
        # Frame-aligned codec streaming transport.
        codec_streaming: true
        # Connector polling / timeout (unit: loop count, sleep interval in seconds).
        connector_get_sleep_s: 0.01
        connector_get_max_wait_first_chunk: 3000
        connector_get_max_wait: 300
        # Align with Omni: small chunks with sufficient context overlap.
        codec_chunk_frames: 25
        codec_left_context_frames: 25

  edges:
    - from: 0
      to: 1
      window_size: -1

qwen3_tts.yaml参数一定要注意设置:engine_args:gpu_memory_utilization: 0.1

四、运行测试

1、启动Qwen3-ASR-1.7B

docker exec -it qwen3-asr bash
cd /home/models/
nohup ./Qwen3-ASR-1.7B.sh &

2、启动Qwen3-TTS-12Hz-1.7B-Base

docker exec -it qwen3-tts bash
cd /home/models/
 nohup ./Qwen3-TTS-12Hz-1.7B-Base.sh &

3、启动语音克隆自编程序

python 语音克隆.py 
 * Serving Flask app '语音克隆'
 * Debug mode: on
WARNING: This is a development server. Do not use it in a production deployment. Use a production WSGI server instead.
 * Running on all addresses (0.0.0.0)
 * Running on http://127.0.0.1:5001
 * Running on http://192.168.222.65:5001
Press CTRL+C to quit
 * Restarting with stat
 * Debugger is active!
 * Debugger PIN: 105-818-056

4、访问语音克隆自编程序

用谷歌浏览器访问,访问之前需要做安全授权设置:

地址栏执行:chrome://flags/

搜索:Insecure origins treated as secure

填入:http://192.168.222.65:5001

然后访问:http://192.168.222.65:5001

先上传自己的语音样本文件或者通过实时录音录制语音文件,通过ASR解析语音样本内容,程序后台将语音文件和解析的语音内容发送给TTS完成语音克隆。

五、测评效果

如果你追求全球化、多语言音频克隆,推荐选择Qwen3-TTS。它拥有10种语言支持和97ms的极低延迟,框架也更通用灵活。其音色克隆质量足够优秀,但在极致相似度上可能略逊于IndexTTS-2。

如果你后续有私有化部署、国产硬件适配、低成本 AI 落地的需求,请务必【关注我】。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐