test-api
This commit is contained in:
parent
67d19911b7
commit
28b23647e6
@ -24,8 +24,10 @@ def create_app(config_name=None):
|
|||||||
# 注册蓝图
|
# 注册蓝图
|
||||||
from app.routes.auth import auth_bp
|
from app.routes.auth import auth_bp
|
||||||
from app.routes.main import main_bp
|
from app.routes.main import main_bp
|
||||||
|
from app.routes.voice_test import voice_test_bp
|
||||||
|
|
||||||
app.register_blueprint(main_bp)
|
app.register_blueprint(main_bp)
|
||||||
app.register_blueprint(auth_bp, url_prefix='/auth')
|
app.register_blueprint(auth_bp, url_prefix='/auth')
|
||||||
|
app.register_blueprint(voice_test_bp, url_prefix='/voice-test')
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|||||||
258
app/routes/voice_test.py
Normal file
258
app/routes/voice_test.py
Normal file
@ -0,0 +1,258 @@
|
|||||||
|
"""
|
||||||
|
语音测试相关路由
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tempfile
|
||||||
|
from flask import Blueprint, request, jsonify, render_template, current_app
|
||||||
|
from flask_login import login_required, current_user
|
||||||
|
from app.services.cosyvoice_service import cosyvoice_service
|
||||||
|
from werkzeug.utils import secure_filename
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
voice_test_bp = Blueprint('voice_test', __name__)
|
||||||
|
|
||||||
|
@voice_test_bp.route('/voice-test')
|
||||||
|
@login_required
|
||||||
|
def voice_test_page():
|
||||||
|
"""语音测试页面"""
|
||||||
|
return render_template('voice_test/index.html')
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/connection', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def test_connection():
|
||||||
|
"""测试CosyVoice服务连接"""
|
||||||
|
try:
|
||||||
|
result = cosyvoice_service.test_connection()
|
||||||
|
return jsonify(result)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"连接测试失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"测试失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/voices', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_voices():
|
||||||
|
"""获取可用音色列表"""
|
||||||
|
try:
|
||||||
|
voices = cosyvoice_service.get_available_voices()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"voices": voices
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取音色列表失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"获取失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/generate/preset', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_with_preset_voice():
|
||||||
|
"""使用预训练音色生成语音"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text', '')
|
||||||
|
voice = data.get('voice', '中文女')
|
||||||
|
seed = data.get('seed', 42)
|
||||||
|
speed = data.get('speed', 1.0)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请输入要合成的文本"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 生成语音
|
||||||
|
stream_audio, full_audio = cosyvoice_service.generate_speech_with_preset_voice(
|
||||||
|
text=text,
|
||||||
|
voice=voice,
|
||||||
|
seed=seed,
|
||||||
|
speed=speed
|
||||||
|
)
|
||||||
|
|
||||||
|
if full_audio:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "语音生成成功",
|
||||||
|
"audio_url": full_audio,
|
||||||
|
"stream_audio_url": stream_audio
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "语音生成失败"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"预训练音色生成失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"生成失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/generate/natural', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_with_natural_control():
|
||||||
|
"""使用自然语言控制生成语音"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text', '')
|
||||||
|
instruction = data.get('instruction', '请用温柔甜美的女声朗读')
|
||||||
|
seed = data.get('seed', 42)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请输入要合成的文本"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 生成语音
|
||||||
|
stream_audio, full_audio = cosyvoice_service.generate_speech_with_natural_control(
|
||||||
|
text=text,
|
||||||
|
instruction=instruction,
|
||||||
|
seed=seed
|
||||||
|
)
|
||||||
|
|
||||||
|
if full_audio:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "语音生成成功",
|
||||||
|
"audio_url": full_audio,
|
||||||
|
"stream_audio_url": stream_audio
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "语音生成失败"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"自然语言控制生成失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"生成失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/upload-audio', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def upload_audio():
|
||||||
|
"""上传音频文件用于语音克隆"""
|
||||||
|
try:
|
||||||
|
if 'audio' not in request.files:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请选择音频文件"
|
||||||
|
})
|
||||||
|
|
||||||
|
file = request.files['audio']
|
||||||
|
if file.filename == '':
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请选择音频文件"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 检查文件类型
|
||||||
|
allowed_extensions = {'wav', 'mp3', 'm4a', 'flac'}
|
||||||
|
if not ('.' in file.filename and
|
||||||
|
file.filename.rsplit('.', 1)[1].lower() in allowed_extensions):
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "不支持的音频格式,请使用WAV、MP3、M4A或FLAC格式"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 保存文件到临时目录
|
||||||
|
filename = secure_filename(file.filename)
|
||||||
|
temp_dir = tempfile.gettempdir()
|
||||||
|
file_path = os.path.join(temp_dir, f"voice_clone_{current_user.id}_{filename}")
|
||||||
|
file.save(file_path)
|
||||||
|
|
||||||
|
# 尝试识别音频内容
|
||||||
|
recognized_text = cosyvoice_service.recognize_audio(file_path)
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "音频上传成功",
|
||||||
|
"file_path": file_path,
|
||||||
|
"recognized_text": recognized_text
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"音频上传失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"上传失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/generate/clone', methods=['POST'])
|
||||||
|
@login_required
|
||||||
|
def generate_with_voice_cloning():
|
||||||
|
"""使用语音克隆生成语音"""
|
||||||
|
try:
|
||||||
|
data = request.get_json()
|
||||||
|
text = data.get('text', '')
|
||||||
|
reference_audio_path = data.get('reference_audio_path', '')
|
||||||
|
reference_text = data.get('reference_text', '')
|
||||||
|
seed = data.get('seed', 42)
|
||||||
|
|
||||||
|
if not text:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请输入要合成的文本"
|
||||||
|
})
|
||||||
|
|
||||||
|
if not reference_audio_path or not os.path.exists(reference_audio_path):
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "请先上传参考音频"
|
||||||
|
})
|
||||||
|
|
||||||
|
# 生成语音
|
||||||
|
stream_audio, full_audio = cosyvoice_service.generate_speech_with_voice_cloning(
|
||||||
|
text=text,
|
||||||
|
reference_audio_path=reference_audio_path,
|
||||||
|
reference_text=reference_text,
|
||||||
|
seed=seed
|
||||||
|
)
|
||||||
|
|
||||||
|
if full_audio:
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"message": "语音克隆成功",
|
||||||
|
"audio_url": full_audio,
|
||||||
|
"stream_audio_url": stream_audio
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": "语音克隆失败"
|
||||||
|
})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"语音克隆失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"克隆失败: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
@voice_test_bp.route('/api/voice-test/random-seed', methods=['GET'])
|
||||||
|
@login_required
|
||||||
|
def get_random_seed():
|
||||||
|
"""获取随机种子"""
|
||||||
|
try:
|
||||||
|
seed = cosyvoice_service.generate_random_seed()
|
||||||
|
return jsonify({
|
||||||
|
"success": True,
|
||||||
|
"seed": seed
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取随机种子失败: {str(e)}")
|
||||||
|
return jsonify({
|
||||||
|
"success": False,
|
||||||
|
"message": f"获取失败: {str(e)}"
|
||||||
|
})
|
||||||
1
app/services/__init__.py
Normal file
1
app/services/__init__.py
Normal file
@ -0,0 +1 @@
|
|||||||
|
# CosyVoice API服务模块
|
||||||
215
app/services/cosyvoice_service.py
Normal file
215
app/services/cosyvoice_service.py
Normal file
@ -0,0 +1,215 @@
|
|||||||
|
"""
|
||||||
|
CosyVoice API 服务类
|
||||||
|
负责与CosyVoice API的交互
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import logging
|
||||||
|
from typing import Optional, Dict, Any, Tuple
|
||||||
|
from gradio_client import Client, handle_file
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class CosyVoiceService:
|
||||||
|
"""CosyVoice API服务类"""
|
||||||
|
|
||||||
|
def __init__(self, api_url: str = "http://127.0.0.1:8080/"):
|
||||||
|
self.api_url = api_url
|
||||||
|
self.client = None
|
||||||
|
|
||||||
|
def connect(self) -> bool:
|
||||||
|
"""连接到CosyVoice服务"""
|
||||||
|
try:
|
||||||
|
self.client = Client(self.api_url)
|
||||||
|
logger.info(f"成功连接到CosyVoice服务: {self.api_url}")
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"连接CosyVoice服务失败: {str(e)}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_available_voices(self) -> list:
|
||||||
|
"""获取可用的音色列表"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return []
|
||||||
|
|
||||||
|
voices = self.client.predict(api_name="/refresh_sft_spk")
|
||||||
|
# 过滤掉不需要的音色
|
||||||
|
filtered_voices = [voice for voice in voices if voice != '.ipynb_checkpoints']
|
||||||
|
return filtered_voices
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取音色列表失败: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_reference_audios(self) -> list:
|
||||||
|
"""获取参考音频列表"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return []
|
||||||
|
|
||||||
|
audio_files = self.client.predict(api_name="/refresh_prompt_wav")
|
||||||
|
return audio_files
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"获取参考音频列表失败: {str(e)}")
|
||||||
|
return []
|
||||||
|
|
||||||
|
def recognize_audio(self, audio_file_path: str) -> str:
|
||||||
|
"""语音识别:将音频转换为文本"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
text = self.client.predict(
|
||||||
|
prompt_wav=handle_file(audio_file_path),
|
||||||
|
api_name="/prompt_wav_recognition"
|
||||||
|
)
|
||||||
|
return text
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"语音识别失败: {str(e)}")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
def generate_speech_with_preset_voice(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
voice: str = "中文女",
|
||||||
|
seed: int = 42,
|
||||||
|
speed: float = 1.0,
|
||||||
|
stream: bool = False
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""使用预训练音色生成语音"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
result = self.client.predict(
|
||||||
|
tts_text=text,
|
||||||
|
mode_checkbox_group="预训练音色",
|
||||||
|
sft_dropdown=voice,
|
||||||
|
seed=seed,
|
||||||
|
speed=speed,
|
||||||
|
stream="true" if stream else "false",
|
||||||
|
api_name="/generate_audio"
|
||||||
|
)
|
||||||
|
|
||||||
|
# result是一个元组 [流式音频路径, 完整音频路径]
|
||||||
|
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
||||||
|
return result[0], result[1]
|
||||||
|
else:
|
||||||
|
return result, result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"预训练音色语音生成失败: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def generate_speech_with_voice_cloning(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
reference_audio_path: str,
|
||||||
|
reference_text: str = "",
|
||||||
|
seed: int = 42
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""使用语音克隆生成语音"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
# 如果没有提供参考文本,先进行语音识别
|
||||||
|
if not reference_text:
|
||||||
|
reference_text = self.recognize_audio(reference_audio_path)
|
||||||
|
if not reference_text:
|
||||||
|
logger.warning("参考音频识别失败,使用空文本")
|
||||||
|
reference_text = ""
|
||||||
|
|
||||||
|
result = self.client.predict(
|
||||||
|
tts_text=text,
|
||||||
|
mode_checkbox_group="3s极速复刻",
|
||||||
|
prompt_text=reference_text,
|
||||||
|
prompt_wav_upload=handle_file(reference_audio_path),
|
||||||
|
seed=seed,
|
||||||
|
api_name="/generate_audio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
||||||
|
return result[0], result[1]
|
||||||
|
else:
|
||||||
|
return result, result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"语音克隆生成失败: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def generate_speech_with_natural_control(
|
||||||
|
self,
|
||||||
|
text: str,
|
||||||
|
instruction: str = "请用温柔甜美的女声朗读",
|
||||||
|
seed: int = 42
|
||||||
|
) -> Tuple[Optional[str], Optional[str]]:
|
||||||
|
"""使用自然语言控制生成语音"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
result = self.client.predict(
|
||||||
|
tts_text=text,
|
||||||
|
mode_checkbox_group="自然语言控制",
|
||||||
|
instruct_text=instruction,
|
||||||
|
seed=seed,
|
||||||
|
api_name="/generate_audio"
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(result, (list, tuple)) and len(result) >= 2:
|
||||||
|
return result[0], result[1]
|
||||||
|
else:
|
||||||
|
return result, result
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"自然语言控制语音生成失败: {str(e)}")
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
def generate_random_seed(self) -> int:
|
||||||
|
"""生成随机种子"""
|
||||||
|
try:
|
||||||
|
if not self.client:
|
||||||
|
if not self.connect():
|
||||||
|
return 42
|
||||||
|
|
||||||
|
seed = self.client.predict(api_name="/generate_random_seed")
|
||||||
|
return int(seed) if seed else 42
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"生成随机种子失败: {str(e)}")
|
||||||
|
return 42
|
||||||
|
|
||||||
|
def test_connection(self) -> Dict[str, Any]:
|
||||||
|
"""测试与CosyVoice服务的连接"""
|
||||||
|
try:
|
||||||
|
if not self.connect():
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": "无法连接到CosyVoice服务",
|
||||||
|
"api_url": self.api_url
|
||||||
|
}
|
||||||
|
|
||||||
|
# 尝试获取音色列表来测试连接
|
||||||
|
voices = self.get_available_voices()
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": "CosyVoice服务连接成功",
|
||||||
|
"api_url": self.api_url,
|
||||||
|
"available_voices": voices
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"message": f"连接测试失败: {str(e)}",
|
||||||
|
"api_url": self.api_url
|
||||||
|
}
|
||||||
|
|
||||||
|
# 全局服务实例
|
||||||
|
cosyvoice_service = CosyVoiceService()
|
||||||
501
app/static/js/voice_test.js
Normal file
501
app/static/js/voice_test.js
Normal file
@ -0,0 +1,501 @@
|
|||||||
|
/**
|
||||||
|
* CosyVoice API 测试页面 JavaScript
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 全局变量
|
||||||
|
let uploadedAudioPath = null;
|
||||||
|
let loadingModal = null;
|
||||||
|
|
||||||
|
// DOM加载完成后初始化
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
initializeComponents();
|
||||||
|
bindEvents();
|
||||||
|
loadAvailableVoices();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化组件
|
||||||
|
*/
|
||||||
|
function initializeComponents() {
|
||||||
|
loadingModal = new bootstrap.Modal(document.getElementById('loadingModal'));
|
||||||
|
|
||||||
|
// 语速滑块显示
|
||||||
|
const speedSlider = document.getElementById('preset-speed');
|
||||||
|
const speedValue = document.getElementById('preset-speed-value');
|
||||||
|
speedSlider.addEventListener('input', function() {
|
||||||
|
speedValue.textContent = this.value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 绑定事件
|
||||||
|
*/
|
||||||
|
function bindEvents() {
|
||||||
|
// 连接测试
|
||||||
|
document.getElementById('test-connection-btn').addEventListener('click', testConnection);
|
||||||
|
|
||||||
|
// 预训练音色测试
|
||||||
|
document.getElementById('preset-voice-form').addEventListener('submit', generatePresetVoice);
|
||||||
|
document.getElementById('preset-random-seed').addEventListener('click', () => getRandomSeed('preset-seed'));
|
||||||
|
|
||||||
|
// 自然语言控制测试
|
||||||
|
document.getElementById('natural-control-form').addEventListener('submit', generateNaturalControl);
|
||||||
|
document.getElementById('natural-random-seed').addEventListener('click', () => getRandomSeed('natural-seed'));
|
||||||
|
|
||||||
|
// 语音克隆测试
|
||||||
|
document.getElementById('audio-upload-form').addEventListener('submit', uploadReferenceAudio);
|
||||||
|
document.getElementById('voice-clone-form').addEventListener('submit', generateVoiceClone);
|
||||||
|
document.getElementById('clone-random-seed').addEventListener('click', () => getRandomSeed('clone-seed'));
|
||||||
|
|
||||||
|
// 清空日志
|
||||||
|
document.getElementById('clear-log').addEventListener('click', clearLog);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示加载状态
|
||||||
|
*/
|
||||||
|
function showLoading(message = '正在处理中...') {
|
||||||
|
document.getElementById('loading-message').textContent = message;
|
||||||
|
loadingModal.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 隐藏加载状态
|
||||||
|
*/
|
||||||
|
function hideLoading() {
|
||||||
|
loadingModal.hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 添加日志
|
||||||
|
*/
|
||||||
|
function addLog(message, type = 'info') {
|
||||||
|
const logContainer = document.getElementById('test-log');
|
||||||
|
const timestamp = new Date().toLocaleTimeString();
|
||||||
|
const logEntry = document.createElement('div');
|
||||||
|
|
||||||
|
const colors = {
|
||||||
|
'info': 'text-primary',
|
||||||
|
'success': 'text-success',
|
||||||
|
'error': 'text-danger',
|
||||||
|
'warning': 'text-warning'
|
||||||
|
};
|
||||||
|
|
||||||
|
logEntry.className = `mb-2 ${colors[type] || 'text-primary'}`;
|
||||||
|
logEntry.innerHTML = `<small>[${timestamp}]</small> ${message}`;
|
||||||
|
|
||||||
|
logContainer.appendChild(logEntry);
|
||||||
|
logContainer.scrollTop = logContainer.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空日志
|
||||||
|
*/
|
||||||
|
function clearLog() {
|
||||||
|
const logContainer = document.getElementById('test-log');
|
||||||
|
logContainer.innerHTML = '<p class="text-muted">测试记录将显示在这里...</p>';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示错误信息
|
||||||
|
*/
|
||||||
|
function showError(message) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast align-items-center text-white bg-danger border-0 position-fixed top-0 end-0 m-3';
|
||||||
|
toast.style.zIndex = '9999';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
<i class="fas fa-exclamation-circle me-2"></i>${message}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
const bsToast = new bootstrap.Toast(toast);
|
||||||
|
bsToast.show();
|
||||||
|
|
||||||
|
// 自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 显示成功信息
|
||||||
|
*/
|
||||||
|
function showSuccess(message) {
|
||||||
|
const toast = document.createElement('div');
|
||||||
|
toast.className = 'toast align-items-center text-white bg-success border-0 position-fixed top-0 end-0 m-3';
|
||||||
|
toast.style.zIndex = '9999';
|
||||||
|
toast.innerHTML = `
|
||||||
|
<div class="d-flex">
|
||||||
|
<div class="toast-body">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>${message}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn-close btn-close-white me-2 m-auto" data-bs-dismiss="toast"></button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
document.body.appendChild(toast);
|
||||||
|
const bsToast = new bootstrap.Toast(toast);
|
||||||
|
bsToast.show();
|
||||||
|
|
||||||
|
// 自动移除
|
||||||
|
setTimeout(() => {
|
||||||
|
if (toast.parentNode) {
|
||||||
|
toast.parentNode.removeChild(toast);
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试连接
|
||||||
|
*/
|
||||||
|
async function testConnection() {
|
||||||
|
const btn = document.getElementById('test-connection-btn');
|
||||||
|
const statusDiv = document.getElementById('connection-status');
|
||||||
|
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.innerHTML = '<i class="fas fa-spinner fa-spin me-2"></i>测试中...';
|
||||||
|
statusDiv.innerHTML = '<span class="text-info">正在测试连接...</span>';
|
||||||
|
|
||||||
|
addLog('开始测试CosyVoice服务连接...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/connection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<span class="text-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>连接成功
|
||||||
|
<small class="text-muted">(${result.api_url})</small>
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
addLog(`连接成功!可用音色数量: ${result.available_voices ? result.available_voices.length : 0}`, 'success');
|
||||||
|
|
||||||
|
// 更新音色列表
|
||||||
|
if (result.available_voices) {
|
||||||
|
updateVoiceOptions(result.available_voices);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<span class="text-danger">
|
||||||
|
<i class="fas fa-times-circle me-2"></i>连接失败: ${result.message}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
addLog(`连接失败: ${result.message}`, 'error');
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
statusDiv.innerHTML = `
|
||||||
|
<span class="text-danger">
|
||||||
|
<i class="fas fa-times-circle me-2"></i>请求失败: ${error.message}
|
||||||
|
</span>
|
||||||
|
`;
|
||||||
|
addLog(`请求失败: ${error.message}`, 'error');
|
||||||
|
} finally {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.innerHTML = '<i class="fas fa-wifi me-2"></i>测试连接';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载可用音色
|
||||||
|
*/
|
||||||
|
async function loadAvailableVoices() {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/voices');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
updateVoiceOptions(result.voices);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载音色失败:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新音色选项
|
||||||
|
*/
|
||||||
|
function updateVoiceOptions(voices) {
|
||||||
|
const voiceSelect = document.getElementById('preset-voice');
|
||||||
|
voiceSelect.innerHTML = '';
|
||||||
|
|
||||||
|
voices.forEach(voice => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = voice;
|
||||||
|
option.textContent = voice;
|
||||||
|
voiceSelect.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取随机种子
|
||||||
|
*/
|
||||||
|
async function getRandomSeed(inputId) {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/random-seed');
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
document.getElementById(inputId).value = result.seed;
|
||||||
|
addLog(`生成随机种子: ${result.seed}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError('获取随机种子失败');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预训练音色语音生成
|
||||||
|
*/
|
||||||
|
async function generatePresetVoice(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const text = document.getElementById('preset-text').value.trim();
|
||||||
|
const voice = document.getElementById('preset-voice').value;
|
||||||
|
const seed = parseInt(document.getElementById('preset-seed').value);
|
||||||
|
const speed = parseFloat(document.getElementById('preset-speed').value);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
showError('请输入要合成的文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在生成语音...');
|
||||||
|
addLog(`开始预训练音色生成 - 音色: ${voice}, 种子: ${seed}, 语速: ${speed}x`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/generate/preset', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
voice: voice,
|
||||||
|
seed: seed,
|
||||||
|
speed: speed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 显示音频播放器
|
||||||
|
const audioSource = document.getElementById('preset-audio-source');
|
||||||
|
const resultDiv = document.getElementById('preset-result');
|
||||||
|
|
||||||
|
audioSource.src = result.audio_url;
|
||||||
|
audioSource.parentElement.load();
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
addLog(`预训练音色生成成功!音频地址: ${result.audio_url}`, 'success');
|
||||||
|
showSuccess('语音生成成功!');
|
||||||
|
} else {
|
||||||
|
addLog(`预训练音色生成失败: ${result.message}`, 'error');
|
||||||
|
showError(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`预训练音色生成出错: ${error.message}`, 'error');
|
||||||
|
showError('生成失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自然语言控制语音生成
|
||||||
|
*/
|
||||||
|
async function generateNaturalControl(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const text = document.getElementById('natural-text').value.trim();
|
||||||
|
const instruction = document.getElementById('natural-instruction').value.trim();
|
||||||
|
const seed = parseInt(document.getElementById('natural-seed').value);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
showError('请输入要合成的文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!instruction) {
|
||||||
|
showError('请输入语音指令');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在生成语音...');
|
||||||
|
addLog(`开始自然语言控制生成 - 指令: ${instruction}, 种子: ${seed}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/generate/natural', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
instruction: instruction,
|
||||||
|
seed: seed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 显示音频播放器
|
||||||
|
const audioSource = document.getElementById('natural-audio-source');
|
||||||
|
const resultDiv = document.getElementById('natural-result');
|
||||||
|
|
||||||
|
audioSource.src = result.audio_url;
|
||||||
|
audioSource.parentElement.load();
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
addLog(`自然语言控制生成成功!音频地址: ${result.audio_url}`, 'success');
|
||||||
|
showSuccess('语音生成成功!');
|
||||||
|
} else {
|
||||||
|
addLog(`自然语言控制生成失败: ${result.message}`, 'error');
|
||||||
|
showError(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`自然语言控制生成出错: ${error.message}`, 'error');
|
||||||
|
showError('生成失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传参考音频
|
||||||
|
*/
|
||||||
|
async function uploadReferenceAudio(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const fileInput = document.getElementById('reference-audio');
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
showError('请选择音频文件');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在上传并识别音频...');
|
||||||
|
addLog(`开始上传音频文件: ${file.name} (${(file.size/1024/1024).toFixed(2)} MB)`);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('audio', file);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/upload-audio', {
|
||||||
|
method: 'POST',
|
||||||
|
body: formData
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 保存音频路径
|
||||||
|
uploadedAudioPath = result.file_path;
|
||||||
|
|
||||||
|
// 显示识别结果
|
||||||
|
const resultDiv = document.getElementById('upload-result');
|
||||||
|
const recognizedText = document.getElementById('recognized-text');
|
||||||
|
|
||||||
|
recognizedText.value = result.recognized_text || '';
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
// 启用克隆按钮
|
||||||
|
const cloneBtn = document.querySelector('#voice-clone-form button[type="submit"]');
|
||||||
|
cloneBtn.disabled = false;
|
||||||
|
|
||||||
|
addLog(`音频上传成功!识别文本: ${result.recognized_text || '(无内容)'}`, 'success');
|
||||||
|
showSuccess('音频上传成功!');
|
||||||
|
} else {
|
||||||
|
addLog(`音频上传失败: ${result.message}`, 'error');
|
||||||
|
showError(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`音频上传出错: ${error.message}`, 'error');
|
||||||
|
showError('上传失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 语音克隆生成
|
||||||
|
*/
|
||||||
|
async function generateVoiceClone(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
if (!uploadedAudioPath) {
|
||||||
|
showError('请先上传参考音频');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = document.getElementById('clone-text').value.trim();
|
||||||
|
const referenceText = document.getElementById('recognized-text').value.trim();
|
||||||
|
const seed = parseInt(document.getElementById('clone-seed').value);
|
||||||
|
|
||||||
|
if (!text) {
|
||||||
|
showError('请输入要合成的文本');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showLoading('正在进行语音克隆...');
|
||||||
|
addLog(`开始语音克隆 - 种子: ${seed}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch('/voice-test/api/voice-test/generate/clone', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
text: text,
|
||||||
|
reference_audio_path: uploadedAudioPath,
|
||||||
|
reference_text: referenceText,
|
||||||
|
seed: seed
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await response.json();
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// 显示音频播放器
|
||||||
|
const audioSource = document.getElementById('clone-audio-source');
|
||||||
|
const resultDiv = document.getElementById('clone-result');
|
||||||
|
|
||||||
|
audioSource.src = result.audio_url;
|
||||||
|
audioSource.parentElement.load();
|
||||||
|
resultDiv.style.display = 'block';
|
||||||
|
|
||||||
|
addLog(`语音克隆成功!音频地址: ${result.audio_url}`, 'success');
|
||||||
|
showSuccess('语音克隆成功!');
|
||||||
|
} else {
|
||||||
|
addLog(`语音克隆失败: ${result.message}`, 'error');
|
||||||
|
showError(result.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
addLog(`语音克隆出错: ${error.message}`, 'error');
|
||||||
|
showError('克隆失败,请检查网络连接');
|
||||||
|
} finally {
|
||||||
|
hideLoading();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -36,6 +36,25 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row g-4 mb-5">
|
<div class="row g-4 mb-5">
|
||||||
|
<div class="col-md-6 col-lg-4">
|
||||||
|
<div class="quick-action-card h-100">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body text-center p-4">
|
||||||
|
<div class="action-icon mb-3">
|
||||||
|
<i class="fas fa-vial text-info"></i>
|
||||||
|
</div>
|
||||||
|
<h5 class="fw-bold mb-2">API 测试</h5>
|
||||||
|
<p class="text-muted small mb-3">
|
||||||
|
测试CosyVoice语音合成功能,体验不同的语音生成模式
|
||||||
|
</p>
|
||||||
|
<a href="{{ url_for('voice_test.voice_test_page') }}" class="btn btn-info">
|
||||||
|
<i class="fas fa-flask me-2"></i>开始测试
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-6 col-lg-4">
|
<div class="col-md-6 col-lg-4">
|
||||||
<div class="quick-action-card h-100">
|
<div class="quick-action-card h-100">
|
||||||
<div class="card border-0 shadow-sm h-100">
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
|||||||
253
app/templates/voice_test/index.html
Normal file
253
app/templates/voice_test/index.html
Normal file
@ -0,0 +1,253 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}CosyVoice API 测试 - 儿童语言学习系统{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container py-4">
|
||||||
|
<!-- 页面标题 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<a href="{{ url_for('main.dashboard') }}" class="btn btn-outline-secondary me-3">
|
||||||
|
<i class="fas fa-arrow-left me-2"></i>返回主页
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<h2 class="fw-bold mb-1">
|
||||||
|
<i class="fas fa-microphone-alt text-primary me-2"></i>
|
||||||
|
CosyVoice API 测试
|
||||||
|
</h2>
|
||||||
|
<p class="text-muted mb-0">测试语音合成的各种功能</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 连接状态检测 -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-plug me-2"></i>服务连接状态
|
||||||
|
</h5>
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<button id="test-connection-btn" class="btn btn-primary me-3">
|
||||||
|
<i class="fas fa-wifi me-2"></i>测试连接
|
||||||
|
</button>
|
||||||
|
<div id="connection-status" class="flex-grow-1">
|
||||||
|
<span class="text-muted">点击测试连接按钮检查服务状态</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 功能测试区域 -->
|
||||||
|
<div class="row">
|
||||||
|
<!-- 预训练音色测试 -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-primary">
|
||||||
|
<i class="fas fa-user-tie me-2"></i>预训练音色测试
|
||||||
|
</h5>
|
||||||
|
<form id="preset-voice-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">要合成的文本</label>
|
||||||
|
<textarea class="form-control" id="preset-text" rows="3"
|
||||||
|
placeholder="输入要转换成语音的文字...">今天天气真不错,适合出门散步。</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">选择音色</label>
|
||||||
|
<select class="form-select" id="preset-voice">
|
||||||
|
<option value="中文女">中文女</option>
|
||||||
|
<option value="中文男">中文男</option>
|
||||||
|
<option value="播报女">播报女</option>
|
||||||
|
<option value="新闻播报男">新闻播报男</option>
|
||||||
|
<option value="英文女">英文女</option>
|
||||||
|
<option value="英文男">英文男</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">随机种子</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" class="form-control" id="preset-seed" value="42">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="preset-random-seed">
|
||||||
|
<i class="fas fa-dice"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6">
|
||||||
|
<label class="form-label">语速</label>
|
||||||
|
<input type="range" class="form-range" id="preset-speed" min="0.5" max="2" step="0.1" value="1">
|
||||||
|
<small class="text-muted">当前: <span id="preset-speed-value">1.0</span>x</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100">
|
||||||
|
<i class="fas fa-play me-2"></i>生成语音
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="preset-result" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>生成成功!
|
||||||
|
<audio controls class="d-block mt-2 w-100">
|
||||||
|
<source id="preset-audio-source" type="audio/wav">
|
||||||
|
您的浏览器不支持音频播放。
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 自然语言控制测试 -->
|
||||||
|
<div class="col-lg-6 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm h-100">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-success">
|
||||||
|
<i class="fas fa-comments me-2"></i>自然语言控制测试
|
||||||
|
</h5>
|
||||||
|
<form id="natural-control-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">要合成的文本</label>
|
||||||
|
<textarea class="form-control" id="natural-text" rows="3"
|
||||||
|
placeholder="输入要转换成语音的文字...">这是一段测试文本,用于验证自然语言控制功能。</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">语音指令</label>
|
||||||
|
<textarea class="form-control" id="natural-instruction" rows="2"
|
||||||
|
placeholder="描述你想要的语音风格...">请用温柔甜美的女声朗读</textarea>
|
||||||
|
<div class="form-text">
|
||||||
|
示例:请用活泼开朗的语调、请用严肃正式的男声、请用轻柔的语气等
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">随机种子</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" class="form-control" id="natural-seed" value="42">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="natural-random-seed">
|
||||||
|
<i class="fas fa-dice"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-success w-100">
|
||||||
|
<i class="fas fa-magic me-2"></i>生成语音
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="natural-result" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>生成成功!
|
||||||
|
<audio controls class="d-block mt-2 w-100">
|
||||||
|
<source id="natural-audio-source" type="audio/wav">
|
||||||
|
您的浏览器不支持音频播放。
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 语音克隆测试 -->
|
||||||
|
<div class="col-12 mb-4">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title text-warning">
|
||||||
|
<i class="fas fa-clone me-2"></i>语音克隆测试
|
||||||
|
</h5>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h6 class="fw-bold mb-3">1. 上传参考音频</h6>
|
||||||
|
<form id="audio-upload-form" enctype="multipart/form-data">
|
||||||
|
<div class="mb-3">
|
||||||
|
<input type="file" class="form-control" id="reference-audio"
|
||||||
|
accept=".wav,.mp3,.m4a,.flac" required>
|
||||||
|
<div class="form-text">
|
||||||
|
支持格式:WAV、MP3、M4A、FLAC,建议3-10秒
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-outline-warning">
|
||||||
|
<i class="fas fa-upload me-2"></i>上传并识别
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="upload-result" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<h6><i class="fas fa-text-width me-2"></i>识别的文本内容:</h6>
|
||||||
|
<textarea class="form-control mt-2" id="recognized-text" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-6">
|
||||||
|
<h6 class="fw-bold mb-3">2. 生成克隆语音</h6>
|
||||||
|
<form id="voice-clone-form">
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">要合成的文本</label>
|
||||||
|
<textarea class="form-control" id="clone-text" rows="3"
|
||||||
|
placeholder="输入要用克隆声音说的话...">这是使用克隆声音合成的新内容。</textarea>
|
||||||
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">随机种子</label>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="number" class="form-control" id="clone-seed" value="42">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" id="clone-random-seed">
|
||||||
|
<i class="fas fa-dice"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-warning w-100" disabled>
|
||||||
|
<i class="fas fa-clone me-2"></i>生成克隆语音
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div id="clone-result" class="mt-3" style="display: none;">
|
||||||
|
<div class="alert alert-success">
|
||||||
|
<i class="fas fa-check-circle me-2"></i>克隆成功!
|
||||||
|
<audio controls class="d-block mt-2 w-100">
|
||||||
|
<source id="clone-audio-source" type="audio/wav">
|
||||||
|
您的浏览器不支持音频播放。
|
||||||
|
</audio>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 测试记录区域 -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-0 shadow-sm">
|
||||||
|
<div class="card-body">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-history me-2"></i>测试记录
|
||||||
|
<button class="btn btn-outline-secondary btn-sm float-end" id="clear-log">
|
||||||
|
<i class="fas fa-trash me-1"></i>清空
|
||||||
|
</button>
|
||||||
|
</h5>
|
||||||
|
<div id="test-log" class="border rounded p-3" style="height: 300px; overflow-y: auto; background-color: #f8f9fa;">
|
||||||
|
<p class="text-muted">测试记录将显示在这里...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Modal -->
|
||||||
|
<div class="modal fade" id="loadingModal" data-bs-backdrop="static" data-bs-keyboard="false" tabindex="-1">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-body text-center py-4">
|
||||||
|
<div class="spinner-border text-primary mb-3" role="status"></div>
|
||||||
|
<h5 id="loading-message">正在处理中...</h5>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script src="{{ url_for('static', filename='js/voice_test.js') }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@ -9,3 +9,4 @@ python-dotenv==1.0.0
|
|||||||
email-validator==2.0.0
|
email-validator==2.0.0
|
||||||
cryptography==41.0.4
|
cryptography==41.0.4
|
||||||
Werkzeug==2.3.7
|
Werkzeug==2.3.7
|
||||||
|
gradio_client==0.8.1
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user