This commit is contained in:
2025-09-02 07:36:42 +08:00
parent 1b959b3ba9
commit 201241571b
5 changed files with 189 additions and 157 deletions

View File

@@ -2,7 +2,7 @@ import logging
import uuid
from typing import Optional
from fastapi import APIRouter, HTTPException
from fastapi import APIRouter, HTTPException, Query # 添加Query导入
from pydantic import BaseModel
from Util.GengerateAudio import ByteDanceTTS
@@ -36,17 +36,13 @@ class TextToSpeechResponse(BaseModel):
request_id: Optional[str] = None
@router.get("/voices/categories")
@router.get("/voice-categories")
async def get_voice_categories():
"""
获取所有音色分类接口
返回所有可用的音色分类列表
"""
try:
categories = tts_instance.get_all_categories()
return {
"success": True,
"data": categories,
"data": categories, # 恢复为原始的 data 字段
"message": "获取音色分类成功"
}
except Exception as e:
@@ -56,15 +52,9 @@ async def get_voice_categories():
detail=f"获取音色分类失败: {str(e)}"
)
@router.get("/voices/by-category/{category}")
async def get_voices_by_category(category: str):
"""
根据分类获取音色列表接口
Args:
category: 音色分类名称
返回指定分类下的所有音色列表
"""
# 恢复原始的音色列表接口路由
@router.get("/voices")
async def get_voices_by_category(category: str = Query(...)): # 现在Query已定义
try:
voices = tts_instance.get_voices_by_category(category)
if not voices:
@@ -86,7 +76,7 @@ async def get_voices_by_category(category: str):
)
@router.get("/voices/all")
@router.get("/all")
async def get_all_voices():
"""
获取所有音色分类和音色列表接口

View File

@@ -26,44 +26,78 @@ class ByteDanceTTS:
# 音色分类字典
TTS_VOICES = {
"通用场景": {
"zh_female_xiaoxue_moon_bigtts": "小雪(女声,温柔亲切)",
"zh_male_xiaofeng_common": "小峰(男声,沉稳大气)",
"zh_female_xiaoxin_common": "小新(女声,自然流畅)",
"zh_male_xiaoyu_common": "小鱼(男声,年轻活力)"
"BV700_V2_streaming": "灿灿 2.0",
"BV705_streaming": "炀炀",
"BV701_V2_streaming": "擎苍 2.0",
"BV001_V2_streaming": "通用女声 2.0",
"BV700_streaming": "灿灿",
"BV406_V2_streaming": "超自然音色-梓梓2.0",
"BV406_streaming": "超自然音色-梓梓",
"BV407_V2_streaming": "超自然音色-燃燃2.0",
"BV407_streaming": "超自然音色-燃燃",
"BV001_streaming": "通用女声12种情感",
"BV002_streaming": "通用男声"
},
"有声阅读": {
"zh_female_xiaoxue_moon_bigtts": "小雪(女声,温柔亲切)",
"zh_female_xiaoxin_common": "小新(女声,自然流畅)",
"zh_female_xiaomei_moon_bigtts": "小美(女声,甜美温柔)",
"zh_female_xiaoli_moon_bigtts": "小丽(女声,清晰标准)"
"BV701_streaming": "擎苍",
"BV123_streaming": "阳光青年",
"BV120_streaming": "反卷青年",
"BV119_streaming": "通用赘婿",
"BV115_streaming": "古风少御",
"BV107_streaming": "霸气青叔",
"BV100_streaming": "质朴青年",
"BV104_streaming": "温柔淑女",
"BV004_streaming": "开朗青年",
"BV113_streaming": "甜宠少御",
"BV102_streaming": "儒雅青年"
},
"智能助手": {
"zh_female_xiaoxue_moon_bigtts": "小雪(女声,温柔亲切)",
"zh_male_xiaofeng_common": "小峰(男声,沉稳大气)",
"zh_female_xiaoxin_common": "小新(女声,自然流畅)",
"zh_male_xiaoyu_common": "小鱼(男声,年轻活力)"
"BV405_streaming": "甜美小源",
"BV007_streaming": "亲切女声",
"BV009_streaming": "知性女声",
"BV419_streaming": "诚诚",
"BV415_streaming": "童童",
"BV008_streaming": "亲切男声"
},
"视频配音": {
"zh_male_xiaofeng_common": "小峰(男声,沉稳大气)",
"zh_female_xiaomei_moon_bigtts": "小美(女声,甜美温柔)",
"zh_female_xiaoli_moon_bigtts": "小丽(女声,清晰标准)",
"zh_male_xiaoyu_common": "小鱼(男声,年轻活力)"
"BV408_streaming": "译制片男声",
"BV426_streaming": "懒小羊",
"BV428_streaming": "清新文艺女声",
"BV403_streaming": "鸡汤女声",
"BV158_streaming": "智慧老者",
"BV157_streaming": "慈爱姥姥",
"BR001_streaming": "说唱小哥",
"BV410_streaming": "活力解说男",
"BV411_streaming": "影视解说小帅",
"BV437_streaming": "解说小帅-多情感",
"BV412_streaming": "影视解说小美",
"BV159_streaming": "纨绔青年",
"BV418_streaming": "直播一姐",
"BV142_streaming": "沉稳解说男",
"BV143_streaming": "潇洒青年",
"BV056_streaming": "阳光男声",
"BV005_streaming": "活泼女声",
"BV064_streaming": "小萝莉"
},
"特色音色": {
"zh_female_xiaoxue_moon_bigtts": "小雪(女声,温柔亲切)",
"zh_female_xiaomei_moon_bigtts": "小美(女声,甜美温柔)"
"BV051_streaming": "奶气萌娃",
"BV063_streaming": "动漫海绵",
"BV417_streaming": "动漫海星",
"BV050_streaming": "动漫小新",
"BV061_streaming": "天才童声"
},
"广告配音": {
"zh_male_xiaofeng_common": "小峰(男声,沉稳大气)",
"zh_female_xiaoli_moon_bigtts": "小丽(女声,清晰标准)"
"BV401_streaming": "促销男声",
"BV402_streaming": "促销女声",
"BV006_streaming": "磁性男声"
},
"新闻播报": {
"zh_female_xiaoli_moon_bigtts": "小丽(女声,清晰标准)",
"zh_male_xiaofeng_common": "小峰(男声,沉稳大气)"
"BV011_streaming": "新闻女声",
"BV012_streaming": "新闻男声"
},
"教育场景": {
"zh_female_xiaoxin_common": "小新(女声,自然流畅)",
"zh_male_xiaoyu_common": "小鱼(男声,年轻活力)"
"BV034_streaming": "知性姐姐-双语",
"BV033_streaming": "温柔小哥"
}
}

View File

@@ -158,6 +158,12 @@
min-height: 100px;
}
.voice-options p {
color: #666;
text-align: center;
padding: 20px;
}
.voice-card {
border: 1px solid #ddd;
border-radius: 8px;
@@ -166,12 +172,6 @@
transition: all 0.3s;
}
.voice-options p {
color: #666;
text-align: center;
padding: 20px;
}
.voice-card:hover {
border-color: #3498db;
box-shadow: 0 2px 8px rgba(52, 152, 219, 0.2);
@@ -242,7 +242,7 @@
<div class="form-group">
<label for="text-input">输入文本</label>
<textarea id="text-input" placeholder="请输入要转换为语音的文本..."></textarea>
<textarea id="text-input" placeholder="请输入要转换为语音的文本...">海上升明月,天涯共此时。</textarea>
</div>
<div class="form-group">
@@ -321,105 +321,121 @@
// 当前选中的音色
let selectedVoiceType = null;
// API基础URL
const apiBaseUrl = '/api/VideoRetalk';
// API基础URL - 使用完整URL
const apiBaseUrl = window.location.origin + '/api/tts';
// 显示错误信息
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
successMessage.style.display = 'none';
}
// 显示成功信息
function showSuccess(message) {
successMessage.textContent = message;
successMessage.style.display = 'block';
errorMessage.style.display = 'none';
}
// 隐藏所有消息
function hideMessages() {
errorMessage.style.display = 'none';
successMessage.style.display = 'none';
}
// 获取所有音色分类
async function loadVoiceCategories() {
try {
const response = await fetch(`${apiBaseUrl}/voices/categories`);
console.log('正在加载音色分类API地址:', `${apiBaseUrl}/voice-categories`);
const response = await fetch(`${apiBaseUrl}/voice-categories`);
if (!response.ok) {
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('音色分类数据:', data);
if (data.success) {
// 清空现有选项
categorySelect.innerHTML = '<option value="">请选择音色分类</option>';
// 清空下拉列表
categorySelect.innerHTML = '<option value="">请选择音色分类</option>';
// 添加分类选项
// 检查返回数据结构
if (data.success && data.data && Array.isArray(data.data) && data.data.length > 0) {
data.data.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.value = category; // 直接使用分类名称作为值
option.textContent = category;
categorySelect.appendChild(option);
});
} else {
showError('获取音色分类失败: ' + data.message);
showError('未能加载音色分类列表');
console.error('音色分类数据格式不正确:', data);
}
// 绑定分类变化事件(确保只绑定一次)
if (!categorySelect.dataset.eventBound) {
categorySelect.addEventListener('change', function() {
if (this.value) {
loadVoicesByCategory(this.value);
} else {
voiceOptions.innerHTML = '<p>请先选择音色分类</p>';
selectedVoiceType = null;
}
});
categorySelect.dataset.eventBound = 'true';
}
} catch (error) {
showError('获取音色分类失败: ' + error.message);
console.error('加载音色分类失败:', error);
showError(`加载音色分类失败: ${error.message}`);
}
}
// 根据分类获取音色列表
async function loadVoicesByCategory(category) {
// 加载指定分类的音色
async function loadVoicesByCategory(categoryId) {
try {
const response = await fetch(`${apiBaseUrl}/voices/by-category/${category}`);
console.log('正在加载音色列表,分类:', categoryId);
const response = await fetch(`${apiBaseUrl}/voices?category=${encodeURIComponent(categoryId)}`);
if (!response.ok) {
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('音色列表数据:', data);
voiceOptions.innerHTML = '';
if (data.success) {
// 清空现有音色选项
voiceOptions.innerHTML = '';
// 添加数据类型检查
if (typeof data.data !== 'object' || data.data === null) {
showError('获取的音色列表格式不正确');
return;
}
// 将对象转换为数组格式 [{voice_type, name, description}, ...]
const voicesArray = Object.entries(data.data).map(([voiceType, description]) => {
// 从描述中提取名称和说明(假设格式为 "名称(说明)"
const match = description.match(/^(.*?)\((.*?)\)$/);
return {
voice_type: voiceType,
name: match ? match[1] : description,
description: match ? match[2] : '无描述'
};
});
// 检查数组是否为空
if (voicesArray.length === 0) {
voiceOptions.innerHTML = '<p>该分类下没有可用音色</p>';
return;
}
// 添加音色卡片
voicesArray.forEach(voice => {
// 检查返回数据结构
if (data.success && data.data && typeof data.data === 'object' && Object.keys(data.data).length > 0) {
Object.entries(data.data).forEach(([voiceId, voiceName]) => {
const voiceCard = document.createElement('div');
voiceCard.className = 'voice-card';
voiceCard.dataset.voiceType = voice.voice_type;
voiceCard.dataset.voiceType = voiceId;
const voiceName = document.createElement('div');
voiceName.className = 'voice-name';
voiceName.textContent = voice.name;
voiceCard.innerHTML = `
<div class="voice-name">${voiceName}</div>
`;
const voiceDescription = document.createElement('div');
voiceDescription.className = 'voice-description';
voiceDescription.textContent = voice.description || '暂无描述';
voiceCard.appendChild(voiceName);
voiceCard.appendChild(voiceDescription);
// 添加点击事件
voiceCard.addEventListener('click', function() {
// 移除其他卡片的选中状态
// 移除其他选中状态
document.querySelectorAll('.voice-card').forEach(card => {
card.classList.remove('selected');
});
// 添加当前卡片的选中状态
// 设置当前选中状态
this.classList.add('selected');
// 保存选中的音色类型
selectedVoiceType = this.dataset.voiceType;
console.log('已选择音色:', selectedVoiceType);
});
voiceOptions.appendChild(voiceCard);
});
} else {
voiceOptions.innerHTML = '<p>获取音色列表失败: ' + data.message + '</p>';
voiceOptions.innerHTML = '<p>该分类下没有可用音色</p>';
console.error('音色列表数据格式不正确:', data);
}
} catch (error) {
voiceOptions.innerHTML = '<p>获取音色列表失败: ' + error.message + '</p>';
console.error('加载音色列表失败:', error);
showError('加载音色列表失败,请重试');
}
}
@@ -456,8 +472,10 @@
encoding: 'mp3'
};
console.log('正在生成音频,请求数据:', requestData);
// 发送请求
const response = await fetch(`${apiBaseUrl}/tts`, {
const response = await fetch(`${apiBaseUrl}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
@@ -466,6 +484,7 @@
});
const data = await response.json();
console.log('音频生成结果:', data);
if (data.success) {
// 显示成功消息
@@ -477,61 +496,50 @@
// 设置下载按钮
downloadBtn.onclick = function() {
const a = document.createElement('a');
a.href = data.audio_url;
a.download = 'tts_audio.mp3';
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
if (!data.audio_url) {
showError('没有找到音频文件');
return;
}
// 创建有意义的文件名 (使用文本前10个字符 + 时间戳)
const textPreview = textInput.value.trim().substring(0, 10).replace(/[^a-zA-Z0-9]/g, '_');
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
const fileName = `tts_${textPreview}_${timestamp}.mp3`;
try {
const a = document.createElement('a');
a.href = data.audio_url;
a.download = fileName;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} catch (error) {
console.error('下载失败:', error);
showError('下载失败,请重试');
}
};
} else {
showError('语音生成失败: ' + data.message);
emptyResult.style.display = 'block';
showError(data.message || '语音生成失败');
}
} catch (error) {
showError('语音生成失败: ' + error.message);
emptyResult.style.display = 'block';
console.error('生成语音失败:', error);
showError('生成语音失败,请重试');
} finally {
// 隐藏加载状态
// 恢复按钮状态
loading.classList.remove('active');
generateBtn.disabled = false;
// 如果没有音频结果,显示空状态
if (audioResult.style.display === 'none') {
emptyResult.style.display = 'block';
}
}
}
// 显示错误消息
function showError(message) {
errorMessage.textContent = message;
errorMessage.style.display = 'block';
successMessage.style.display = 'none';
}
// 显示成功消息
function showSuccess(message) {
successMessage.textContent = message;
successMessage.style.display = 'block';
errorMessage.style.display = 'none';
}
// 隐藏所有消息
function hideMessages() {
errorMessage.style.display = 'none';
successMessage.style.display = 'none';
}
// 事件监听器
categorySelect.addEventListener('change', function() {
const category = this.value;
if (category) {
loadVoicesByCategory(category);
} else {
voiceOptions.innerHTML = '<p>请先选择音色分类</p>';
selectedVoiceType = null;
}
});
// 绑定生成按钮点击事件
generateBtn.addEventListener('click', generateAudio);
// 初始化
// 初始化加载音色分类
loadVoiceCategories();
});
</script>