2025-09-02 06:55:13 +08:00
|
|
|
|
<!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;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
.voice-options p {
|
|
|
|
|
color: #666;
|
|
|
|
|
text-align: center;
|
|
|
|
|
padding: 20px;
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 06:55:13 +08:00
|
|
|
|
.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>
|
2025-09-02 07:36:42 +08:00
|
|
|
|
<textarea id="text-input" placeholder="请输入要转换为语音的文本...">海上升明月,天涯共此时。</textarea>
|
2025-09-02 06:55:13 +08:00
|
|
|
|
</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;
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 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';
|
|
|
|
|
}
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
|
|
|
|
// 获取所有音色分类
|
|
|
|
|
async function loadVoiceCategories() {
|
|
|
|
|
try {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('正在加载音色分类,API地址:', `${apiBaseUrl}/voice-categories`);
|
|
|
|
|
const response = await fetch(`${apiBaseUrl}/voice-categories`);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 06:55:13 +08:00
|
|
|
|
const data = await response.json();
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('音色分类数据:', data);
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 清空下拉列表
|
|
|
|
|
categorySelect.innerHTML = '<option value="">请选择音色分类</option>';
|
|
|
|
|
|
|
|
|
|
// 检查返回数据结构
|
|
|
|
|
if (data.success && data.data && Array.isArray(data.data) && data.data.length > 0) {
|
2025-09-02 06:55:13 +08:00
|
|
|
|
data.data.forEach(category => {
|
|
|
|
|
const option = document.createElement('option');
|
2025-09-02 07:36:42 +08:00
|
|
|
|
option.value = category; // 直接使用分类名称作为值
|
2025-09-02 06:55:13 +08:00
|
|
|
|
option.textContent = category;
|
|
|
|
|
categorySelect.appendChild(option);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
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';
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.error('加载音色分类失败:', error);
|
|
|
|
|
showError(`加载音色分类失败: ${error.message}`);
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 加载指定分类的音色
|
|
|
|
|
async function loadVoicesByCategory(categoryId) {
|
2025-09-02 06:55:13 +08:00
|
|
|
|
try {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('正在加载音色列表,分类:', categoryId);
|
|
|
|
|
const response = await fetch(`${apiBaseUrl}/voices?category=${encodeURIComponent(categoryId)}`);
|
|
|
|
|
|
|
|
|
|
if (!response.ok) {
|
|
|
|
|
throw new Error(`API请求失败: ${response.status} ${response.statusText}`);
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 06:55:13 +08:00
|
|
|
|
const data = await response.json();
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('音色列表数据:', data);
|
|
|
|
|
voiceOptions.innerHTML = '';
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 检查返回数据结构
|
|
|
|
|
if (data.success && data.data && typeof data.data === 'object' && Object.keys(data.data).length > 0) {
|
|
|
|
|
Object.entries(data.data).forEach(([voiceId, voiceName]) => {
|
2025-09-02 06:55:13 +08:00
|
|
|
|
const voiceCard = document.createElement('div');
|
|
|
|
|
voiceCard.className = 'voice-card';
|
2025-09-02 07:36:42 +08:00
|
|
|
|
voiceCard.dataset.voiceType = voiceId;
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
voiceCard.innerHTML = `
|
|
|
|
|
<div class="voice-name">${voiceName}</div>
|
|
|
|
|
`;
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
|
|
|
|
voiceCard.addEventListener('click', function() {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 移除其他选中状态
|
2025-09-02 06:55:13 +08:00
|
|
|
|
document.querySelectorAll('.voice-card').forEach(card => {
|
|
|
|
|
card.classList.remove('selected');
|
|
|
|
|
});
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 设置当前选中状态
|
2025-09-02 06:55:13 +08:00
|
|
|
|
this.classList.add('selected');
|
|
|
|
|
selectedVoiceType = this.dataset.voiceType;
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('已选择音色:', selectedVoiceType);
|
2025-09-02 06:55:13 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
voiceOptions.appendChild(voiceCard);
|
|
|
|
|
});
|
|
|
|
|
} else {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
voiceOptions.innerHTML = '<p>该分类下没有可用音色</p>';
|
|
|
|
|
console.error('音色列表数据格式不正确:', data);
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.error('加载音色列表失败:', error);
|
|
|
|
|
showError('加载音色列表失败,请重试');
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 生成语音
|
|
|
|
|
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'
|
|
|
|
|
};
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('正在生成音频,请求数据:', requestData);
|
|
|
|
|
|
2025-09-02 06:55:13 +08:00
|
|
|
|
// 发送请求
|
2025-09-02 07:36:42 +08:00
|
|
|
|
const response = await fetch(`${apiBaseUrl}/generate`, {
|
2025-09-02 06:55:13 +08:00
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: {
|
|
|
|
|
'Content-Type': 'application/json'
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify(requestData)
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const data = await response.json();
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.log('音频生成结果:', data);
|
2025-09-02 06:55:13 +08:00
|
|
|
|
|
2025-09-02 08:23:33 +08:00
|
|
|
|
if (data.status==='success') {
|
2025-09-02 06:55:13 +08:00
|
|
|
|
// 显示成功消息
|
|
|
|
|
showSuccess('语音生成成功');
|
|
|
|
|
|
|
|
|
|
// 设置音频播放器
|
2025-09-02 08:23:33 +08:00
|
|
|
|
if(data.audio_url){
|
|
|
|
|
console.log('音频文件地址:', data.audio_url)
|
|
|
|
|
}
|
2025-09-02 06:55:13 +08:00
|
|
|
|
audioPlayer.src = data.audio_url;
|
|
|
|
|
audioResult.style.display = 'block';
|
|
|
|
|
|
|
|
|
|
// 设置下载按钮
|
|
|
|
|
downloadBtn.onclick = function() {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
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');
|
2025-09-02 08:48:33 +08:00
|
|
|
|
// 移除本地代理,直接使用OBS的HTTPS链接
|
|
|
|
|
// 同时添加URL清理逻辑,移除可能存在的引号和空格
|
|
|
|
|
a.href = data.audio_url.trim().replace(/[`'"\s]/g, '');
|
|
|
|
|
a.download = data.filename;
|
2025-09-02 07:36:42 +08:00
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
a.click();
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('下载失败:', error);
|
|
|
|
|
showError('下载失败,请重试');
|
|
|
|
|
}
|
2025-09-02 06:55:13 +08:00
|
|
|
|
};
|
|
|
|
|
} else {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
showError(data.message || '语音生成失败');
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
console.error('生成语音失败:', error);
|
|
|
|
|
showError('生成语音失败,请重试');
|
2025-09-02 06:55:13 +08:00
|
|
|
|
} finally {
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 恢复按钮状态
|
2025-09-02 06:55:13 +08:00
|
|
|
|
loading.classList.remove('active');
|
|
|
|
|
generateBtn.disabled = false;
|
2025-09-02 07:36:42 +08:00
|
|
|
|
|
|
|
|
|
// 如果没有音频结果,显示空状态
|
|
|
|
|
if (audioResult.style.display === 'none') {
|
|
|
|
|
emptyResult.style.display = 'block';
|
|
|
|
|
}
|
2025-09-02 06:55:13 +08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 绑定生成按钮点击事件
|
2025-09-02 06:55:13 +08:00
|
|
|
|
generateBtn.addEventListener('click', generateAudio);
|
|
|
|
|
|
2025-09-02 07:36:42 +08:00
|
|
|
|
// 初始化加载音色分类
|
2025-09-02 06:55:13 +08:00
|
|
|
|
loadVoiceCategories();
|
|
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
</body>
|
|
|
|
|
</html>
|