Files
dsProject/dsLightRag/static/text-to-speech.html
2025-09-02 06:55:13 +08:00

539 lines
20 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文本转语音</title>
<style>
* {
margin: 0; padding: 0; box-sizing: border-box;
font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
}
body {
background-color: #f5f7fa;
color: #333;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
text-align: center;
padding: 40px 20px;
background: linear-gradient(135deg, #3498db, #8e44ad);
color: white;
border-radius: 10px;
margin-bottom: 30px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
}
h1 {
font-size: 2.2rem;
margin-bottom: 10px;
font-weight: 700;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
max-width: 800px;
margin: 0 auto;
}
.main-content {
display: flex;
flex-wrap: wrap;
gap: 30px;
margin-bottom: 30px;
}
.form-section {
flex: 1;
min-width: 300px;
background-color: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.result-section {
flex: 1;
min-width: 300px;
background-color: white;
border-radius: 10px;
padding: 25px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #2c3e50;
}
select, textarea {
width: 100%;
padding: 12px;
border: 1px solid #ddd;
border-radius: 6px;
font-size: 16px;
transition: border 0.3s;
}
select:focus, textarea:focus {
border-color: #3498db;
outline: none;
}
textarea {
min-height: 150px;
resize: vertical;
}
.btn {
padding: 12px 24px;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 16px;
transition: background 0.3s;
display: inline-block;
text-align: center;
}
.btn-primary {
background-color: #3498db;
color: white;
}
.btn-primary:hover {
background-color: #2980b9;
}
.btn:disabled {
background-color: #95a5a6;
cursor: not-allowed;
}
.audio-player {
margin-top: 20px;
width: 100%;
}
.loading {
display: none;
text-align: center;
margin: 20px 0;
}
.loading.active {
display: block;
}
.spinner {
border: 4px solid rgba(0, 0, 0, 0.1);
border-radius: 50%;
border-top: 4px solid #3498db;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.voice-options {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
margin-top: 15px;
min-height: 100px;
}
.voice-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
cursor: pointer;
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);
}
.voice-card.selected {
border-color: #3498db;
background-color: #e8f4fd;
}
.voice-name {
font-weight: 500;
margin-bottom: 5px;
}
.voice-description {
font-size: 0.9rem;
color: #666;
}
.error-message {
color: #e74c3c;
margin-top: 10px;
display: none;
}
.success-message {
color: #2ecc71;
margin-top: 10px;
display: none;
}
@media screen and (max-width: 768px) {
.main-content {
flex-direction: column;
}
h1 {
font-size: 1.8rem;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>文本转语音</h1>
<p class="subtitle">选择音色,输入文本,生成高质量语音</p>
</header>
<div class="main-content">
<div class="form-section">
<h2>语音设置</h2>
<div class="form-group">
<label for="category-select">音色分类</label>
<select id="category-select">
<option value="">请选择音色分类</option>
</select>
</div>
<div class="form-group">
<label>选择音色</label>
<div id="voice-options" class="voice-options">
<p>请先选择音色分类</p>
</div>
</div>
<div class="form-group">
<label for="text-input">输入文本</label>
<textarea id="text-input" placeholder="请输入要转换为语音的文本..."></textarea>
</div>
<div class="form-group">
<label for="speed-ratio">语速</label>
<select id="speed-ratio">
<option value="0.8">较慢</option>
<option value="1.0" selected>正常</option>
<option value="1.2">较快</option>
</select>
</div>
<div class="form-group">
<label for="volume-ratio">音量</label>
<select id="volume-ratio">
<option value="0.8">较小</option>
<option value="1.0" selected>正常</option>
<option value="1.2">较大</option>
</select>
</div>
<div class="form-group">
<label for="pitch-ratio">音调</label>
<select id="pitch-ratio">
<option value="0.8">较低</option>
<option value="1.0" selected>正常</option>
<option value="1.2">较高</option>
</select>
</div>
<button id="generate-btn" class="btn btn-primary">生成语音</button>
<div id="error-message" class="error-message"></div>
<div id="success-message" class="success-message"></div>
</div>
<div class="result-section">
<h2>生成结果</h2>
<div id="loading" class="loading">
<div class="spinner"></div>
<p>正在生成语音,请稍候...</p>
</div>
<div id="audio-result" style="display: none;">
<audio id="audio-player" class="audio-player" controls></audio>
<div class="form-group" style="margin-top: 20px;">
<button id="download-btn" class="btn btn-primary">下载音频</button>
</div>
</div>
<div id="empty-result" style="text-align: center; padding: 40px 0; color: #999;">
<p>暂无生成结果</p>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// 获取DOM元素
const categorySelect = document.getElementById('category-select');
const voiceOptions = document.getElementById('voice-options');
const textInput = document.getElementById('text-input');
const speedRatio = document.getElementById('speed-ratio');
const volumeRatio = document.getElementById('volume-ratio');
const pitchRatio = document.getElementById('pitch-ratio');
const generateBtn = document.getElementById('generate-btn');
const loading = document.getElementById('loading');
const audioResult = document.getElementById('audio-result');
const audioPlayer = document.getElementById('audio-player');
const downloadBtn = document.getElementById('download-btn');
const emptyResult = document.getElementById('empty-result');
const errorMessage = document.getElementById('error-message');
const successMessage = document.getElementById('success-message');
// 当前选中的音色
let selectedVoiceType = null;
// API基础URL
const apiBaseUrl = '/api/VideoRetalk';
// 获取所有音色分类
async function loadVoiceCategories() {
try {
const response = await fetch(`${apiBaseUrl}/voices/categories`);
const data = await response.json();
if (data.success) {
// 清空现有选项
categorySelect.innerHTML = '<option value="">请选择音色分类</option>';
// 添加分类选项
data.data.forEach(category => {
const option = document.createElement('option');
option.value = category;
option.textContent = category;
categorySelect.appendChild(option);
});
} else {
showError('获取音色分类失败: ' + data.message);
}
} catch (error) {
showError('获取音色分类失败: ' + error.message);
}
}
// 根据分类获取音色列表
async function loadVoicesByCategory(category) {
try {
const response = await fetch(`${apiBaseUrl}/voices/by-category/${category}`);
const data = await response.json();
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 => {
const voiceCard = document.createElement('div');
voiceCard.className = 'voice-card';
voiceCard.dataset.voiceType = voice.voice_type;
const voiceName = document.createElement('div');
voiceName.className = 'voice-name';
voiceName.textContent = voice.name;
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;
});
voiceOptions.appendChild(voiceCard);
});
} else {
voiceOptions.innerHTML = '<p>获取音色列表失败: ' + data.message + '</p>';
}
} catch (error) {
voiceOptions.innerHTML = '<p>获取音色列表失败: ' + error.message + '</p>';
}
}
// 生成语音
async function generateAudio() {
// 验证输入
if (!selectedVoiceType) {
showError('请选择音色');
return;
}
if (!textInput.value.trim()) {
showError('请输入要转换的文本');
return;
}
// 隐藏错误和成功消息
hideMessages();
// 显示加载状态
loading.classList.add('active');
audioResult.style.display = 'none';
emptyResult.style.display = 'none';
generateBtn.disabled = true;
try {
// 准备请求数据
const requestData = {
text: textInput.value.trim(),
voice_type: selectedVoiceType,
speed_ratio: parseFloat(speedRatio.value),
volume_ratio: parseFloat(volumeRatio.value),
pitch_ratio: parseFloat(pitchRatio.value),
encoding: 'mp3'
};
// 发送请求
const response = await fetch(`${apiBaseUrl}/tts`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
if (data.success) {
// 显示成功消息
showSuccess('语音生成成功');
// 设置音频播放器
audioPlayer.src = data.audio_url;
audioResult.style.display = 'block';
// 设置下载按钮
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);
};
} else {
showError('语音生成失败: ' + data.message);
emptyResult.style.display = 'block';
}
} catch (error) {
showError('语音生成失败: ' + error.message);
emptyResult.style.display = 'block';
} finally {
// 隐藏加载状态
loading.classList.remove('active');
generateBtn.disabled = false;
}
}
// 显示错误消息
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>
</body>
</html>