Files
dsProject/dsLightRag/static/text-to-speech.html
2025-09-02 08:48:33 +08:00

551 lines
21 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-options p {
color: #666;
text-align: center;
padding: 20px;
}
.voice-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 15px;
cursor: pointer;
transition: all 0.3s;
}
.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;">
</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 - 使用完整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 {
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);
// 清空下拉列表
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.textContent = category;
categorySelect.appendChild(option);
});
} else {
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) {
console.error('加载音色分类失败:', error);
showError(`加载音色分类失败: ${error.message}`);
}
}
// 加载指定分类的音色
async function loadVoicesByCategory(categoryId) {
try {
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 && 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 = voiceId;
voiceCard.innerHTML = `
<div class="voice-name">${voiceName}</div>
`;
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>该分类下没有可用音色</p>';
console.error('音色列表数据格式不正确:', data);
}
} catch (error) {
console.error('加载音色列表失败:', error);
showError('加载音色列表失败,请重试');
}
}
// 生成语音
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'
};
console.log('正在生成音频,请求数据:', requestData);
// 发送请求
const response = await fetch(`${apiBaseUrl}/generate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(requestData)
});
const data = await response.json();
console.log('音频生成结果:', data);
if (data.status==='success') {
// 显示成功消息
showSuccess('语音生成成功');
// 设置音频播放器
if(data.audio_url){
console.log('音频文件地址:', data.audio_url)
}
audioPlayer.src = data.audio_url;
audioResult.style.display = 'block';
// 设置下载按钮
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');
// 移除本地代理直接使用OBS的HTTPS链接
// 同时添加URL清理逻辑移除可能存在的引号和空格
a.href = data.audio_url.trim().replace(/[`'"\s]/g, '');
a.download = data.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
} catch (error) {
console.error('下载失败:', error);
showError('下载失败,请重试');
}
};
} else {
showError(data.message || '语音生成失败');
}
} catch (error) {
console.error('生成语音失败:', error);
showError('生成语音失败,请重试');
} finally {
// 恢复按钮状态
loading.classList.remove('active');
generateBtn.disabled = false;
// 如果没有音频结果,显示空状态
if (audioResult.style.display === 'none') {
emptyResult.style.display = 'block';
}
}
}
// 绑定生成按钮点击事件
generateBtn.addEventListener('click', generateAudio);
// 初始化加载音色分类
loadVoiceCategories();
});
</script>
</body>
</html>