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

@@ -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;
@@ -165,12 +171,6 @@
cursor: pointer;
transition: all 0.3s;
}
.voice-options p {
color: #666;
text-align: center;
padding: 20px;
}
.voice-card:hover {
border-color: #3498db;
@@ -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) {
// 清空现有选项
categorySelect.innerHTML = '<option value="">请选择音色分类</option>';
// 添加分类选项
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) {
// 清空现有音色选项
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 (!response.ok) {
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
}
const data = await response.json();
console.log('音色列表数据:', data);
voiceOptions.innerHTML = '';
// 检查返回数据结构
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>