|
<!DOCTYPE html> |
|
<html lang="zh-CN"> |
|
|
|
<head> |
|
<meta charset="UTF-8"> |
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
<title>TTS 语音合成服务</title> |
|
<style> |
|
body { |
|
font-family: Arial, sans-serif; |
|
margin: 0; |
|
padding: 20px; |
|
background-color: #f5f5f5; |
|
} |
|
|
|
.container { |
|
max-width: 1200px; |
|
margin: 0 auto; |
|
background-color: white; |
|
padding: 20px; |
|
border-radius: 8px; |
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1); |
|
} |
|
|
|
h1 { |
|
color: #333; |
|
text-align: center; |
|
} |
|
|
|
.filter-section { |
|
margin-bottom: 20px; |
|
padding: 15px; |
|
background-color: #f0f8ff; |
|
border-radius: 5px; |
|
} |
|
|
|
.voice-list { |
|
display: grid; |
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); |
|
gap: 20px; |
|
} |
|
|
|
.voice-card { |
|
border: 1px solid #ddd; |
|
padding: 15px; |
|
border-radius: 5px; |
|
background-color: white; |
|
} |
|
|
|
.voice-card h3 { |
|
margin-top: 0; |
|
color: #2c3e50; |
|
} |
|
|
|
.voice-card p { |
|
margin: 5px 0; |
|
} |
|
|
|
.style-list { |
|
display: flex; |
|
flex-wrap: wrap; |
|
gap: 5px; |
|
margin-top: 10px; |
|
} |
|
|
|
.style-tag { |
|
background-color: #e0f7fa; |
|
padding: 3px 8px; |
|
border-radius: 10px; |
|
font-size: 12px; |
|
} |
|
|
|
select, |
|
button { |
|
padding: 8px 12px; |
|
margin-right: 10px; |
|
border-radius: 4px; |
|
border: 1px solid #ddd; |
|
} |
|
|
|
button { |
|
background-color: #4CAF50; |
|
color: white; |
|
border: none; |
|
cursor: pointer; |
|
} |
|
|
|
button:hover { |
|
background-color: #45a049; |
|
} |
|
</style> |
|
</head> |
|
|
|
<body> |
|
<div class="container"> |
|
<h1>TTS 语音合成服务</h1> |
|
<div class="container" style="margin-top: 30px;"> |
|
<h2>语音合成</h2> |
|
<div class="filter-section"> |
|
<div style="margin-bottom: 15px;"> |
|
<label for="synthesis-text">输入文本:</label> |
|
<textarea id="synthesis-text" rows="4" |
|
style="width: 100%; padding: 8px; border-radius: 4px; border: 1px solid #ddd;"></textarea> |
|
</div> |
|
|
|
<div style="margin-bottom: 15px;"> |
|
<label for="synthesis-voice">选择音色:</label> |
|
<select id="synthesis-voice" style="width: 200px;"> |
|
|
|
</select> |
|
</div> |
|
|
|
<div style="margin-bottom: 15px;"> |
|
<label for="rate-slider">语速: <span id="rate-value">1.0</span></label> |
|
<input type="range" id="rate-slider" min="0.5" max="2.0" step="0.1" value="1.0" |
|
style="width: 200px;" oninput="document.getElementById('rate-value').textContent = this.value"> |
|
</div> |
|
|
|
<div style="margin-bottom: 15px;"> |
|
<label for="pitch-slider">音调: <span id="pitch-value">0</span></label> |
|
<input type="range" id="pitch-slider" min="-12" max="12" step="1" value="0" style="width: 200px;" |
|
oninput="document.getElementById('pitch-value').textContent = this.value"> |
|
</div> |
|
|
|
<button onclick="synthesizeSpeech()">合成语音</button> |
|
<audio id="audio-player" controls style="display: none; margin-top: 15px; width: 100%;"></audio> |
|
</div> |
|
<div class="filter-section"> |
|
<label for="gender-filter">按性别筛选:</label> |
|
<select id="gender-filter"> |
|
<option value="all">全部</option> |
|
<option value="Female">女声</option> |
|
<option value="Male">男声</option> |
|
</select> |
|
<button id="apply-filter">应用筛选</button> |
|
</div> |
|
|
|
<div class="voice-list" id="voice-list"> |
|
|
|
</div> |
|
</div> |
|
|
|
|
|
</div> |
|
|
|
<script> |
|
async function loadVoices(gender = 'all') { |
|
try { |
|
const response = await fetch('/voices?l=zh&d=true'); |
|
let voices = await response.json(); |
|
voices = voices['voices'] |
|
const filteredVoices = gender === 'all' |
|
? voices |
|
: voices.filter(voice => voice.Gender === gender); |
|
|
|
const voiceList = document.getElementById('voice-list'); |
|
voiceList.innerHTML = ''; |
|
|
|
filteredVoices.forEach(voice => { |
|
const voiceCard = document.createElement('div'); |
|
voiceCard.className = 'voice-card'; |
|
|
|
voiceCard.innerHTML = ` |
|
<h3>${voice.LocalName} (${voice.DisplayName})</h3> |
|
<p><strong>性别:</strong> ${voice.Gender === 'Female' ? '女' : '男'}</p> |
|
<p><strong>语言:</strong> ${voice.LocaleName}</p> |
|
<p><strong>短名称:</strong> ${voice.ShortName}</p> |
|
|
|
<div class="style-list"> |
|
${voice.StyleList ? voice.StyleList.map(style => |
|
`<span class="style-tag">${style}</span>`).join('') : ''} |
|
</div> |
|
`; |
|
|
|
voiceList.appendChild(voiceCard); |
|
}); |
|
} catch (error) { |
|
console.error('加载音色列表失败:', error); |
|
document.getElementById('voice-list').innerHTML = |
|
'<p style="color:red;">加载音色列表失败,请刷新页面重试</p>'; |
|
} |
|
} |
|
|
|
async function synthesizeSpeech() { |
|
const text = document.getElementById('synthesis-text').value; |
|
const voice = document.getElementById('synthesis-voice').value; |
|
const rate = document.getElementById('rate-slider').value; |
|
const pitch = document.getElementById('pitch-slider').value; |
|
|
|
try { |
|
const response = await fetch('/tts', { |
|
method: 'POST', |
|
headers: { |
|
'Content-Type': 'application/json', |
|
}, |
|
body: JSON.stringify({ |
|
t: text, |
|
v: voice, |
|
r: rate, |
|
p: pitch |
|
}) |
|
}); |
|
|
|
if (response.ok) { |
|
let audioUrl = await response.json(); |
|
audioUrl = audioUrl['audio_url'] |
|
const audioPlayer = document.getElementById('audio-player'); |
|
audioPlayer.src = audioUrl; |
|
audioPlayer.style.display = 'block'; |
|
} else { |
|
alert('语音合成失败: ' + await response.text()); |
|
} |
|
} catch (error) { |
|
console.error('语音合成错误:', error); |
|
alert('语音合成过程中发生错误'); |
|
} |
|
} |
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
loadVoices(); |
|
|
|
|
|
fetch('/voices?l=zh&d=true') |
|
.then(response => response.json()) |
|
.then(data => { |
|
const voiceSelect = document.getElementById('synthesis-voice'); |
|
data.voices.forEach(voice => { |
|
const option = document.createElement('option'); |
|
option.value = voice.ShortName; |
|
option.textContent = `${voice.LocalName} (${voice.Gender === 'Female' ? '女' : '男'})`; |
|
voiceSelect.appendChild(option); |
|
}); |
|
}); |
|
|
|
document.getElementById('apply-filter').addEventListener('click', () => { |
|
const gender = document.getElementById('gender-filter').value; |
|
loadVoices(gender); |
|
}); |
|
}); |
|
</script> |
|
</body> |
|
|
|
</html> |