'commit'
This commit is contained in:
Binary file not shown.
@@ -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():
|
||||
"""
|
||||
获取所有音色分类和音色列表接口
|
||||
|
@@ -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": "温柔小哥"
|
||||
}
|
||||
}
|
||||
|
||||
|
Binary file not shown.
@@ -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`);
|
||||
const data = await response.json();
|
||||
console.log('正在加载音色分类,API地址:', `${apiBaseUrl}/voice-categories`);
|
||||
const response = await fetch(`${apiBaseUrl}/voice-categories`);
|
||||
|
||||
if (data.success) {
|
||||
// 清空现有选项
|
||||
if (!response.ok) {
|
||||
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('音色分类数据:', data);
|
||||
|
||||
// 清空下拉列表
|
||||
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}`);
|
||||
const data = await response.json();
|
||||
console.log('正在加载音色列表,分类:', categoryId);
|
||||
const response = await fetch(`${apiBaseUrl}/voices?category=${encodeURIComponent(categoryId)}`);
|
||||
|
||||
if (data.success) {
|
||||
// 清空现有音色选项
|
||||
if (!response.ok) {
|
||||
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
console.log('音色列表数据:', data);
|
||||
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() {
|
||||
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 = 'tts_audio.mp3';
|
||||
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>
|
||||
|
Reference in New Issue
Block a user