SchoolPhysicalExamination/application/tsf/view/index/index.html

651 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>百度TTS语音合成工具</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'PingFang SC', 'Microsoft YaHei', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 40px 20px;
}
.container {
max-width: 900px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #fff;
font-size: 32px;
font-weight: 600;
text-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header p {
color: rgba(255,255,255,0.8);
margin-top: 8px;
font-size: 14px;
}
.main-card {
background: #fff;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.15);
overflow: hidden;
}
.card-header {
background: linear-gradient(90deg, #667eea, #764ba2);
padding: 20px 30px;
color: #fff;
}
.card-header h2 {
font-size: 18px;
font-weight: 500;
}
.card-body {
padding: 30px;
}
.form-group {
margin-bottom: 24px;
}
.form-group label {
display: block;
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 10px;
}
.form-group label span {
color: #999;
font-weight: 400;
font-size: 12px;
margin-left: 8px;
}
.text-input {
width: 100%;
padding: 16px;
border: 2px solid #e8e8e8;
border-radius: 10px;
font-size: 16px;
line-height: 1.6;
resize: vertical;
min-height: 150px;
transition: border-color 0.3s;
font-family: inherit;
}
.text-input:focus {
outline: none;
border-color: #667eea;
}
.text-input::placeholder {
color: #bbb;
}
.char-count {
text-align: right;
font-size: 12px;
color: #999;
margin-top: 8px;
}
.config-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
}
.config-item {
background: #f8f9fa;
padding: 16px;
border-radius: 10px;
}
.config-item label {
font-size: 13px;
margin-bottom: 10px;
}
.config-item select,
.config-item input[type="range"] {
width: 100%;
}
select {
padding: 10px 12px;
border: 2px solid #e8e8e8;
border-radius: 8px;
font-size: 14px;
background: #fff;
cursor: pointer;
transition: border-color 0.3s;
}
select:focus {
outline: none;
border-color: #667eea;
}
input[type="range"] {
-webkit-appearance: none;
height: 6px;
background: #e8e8e8;
border-radius: 3px;
cursor: pointer;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 18px;
height: 18px;
background: linear-gradient(135deg, #667eea, #764ba2);
border-radius: 50%;
cursor: pointer;
box-shadow: 0 2px 6px rgba(102,126,234,0.4);
}
.range-value {
text-align: center;
font-size: 14px;
color: #667eea;
font-weight: 600;
margin-top: 8px;
}
.btn-group {
display: flex;
gap: 12px;
margin-top: 30px;
}
.btn {
flex: 1;
padding: 14px 24px;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
.btn-primary {
background: linear-gradient(135deg, #667eea, #764ba2);
color: #fff;
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 20px rgba(102,126,234,0.4);
}
.btn-primary:disabled {
opacity: 0.6;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.btn-secondary {
background: #f0f0f0;
color: #666;
}
.btn-secondary:hover {
background: #e0e0e0;
}
.btn svg {
width: 20px;
height: 20px;
}
.result-section {
margin-top: 30px;
padding-top: 30px;
border-top: 1px solid #eee;
display: none;
}
.result-section.show {
display: block;
}
.result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
}
.result-header h3 {
font-size: 16px;
color: #333;
}
.file-info {
font-size: 12px;
color: #999;
}
.audio-player {
width: 100%;
border-radius: 10px;
outline: none;
}
.download-link {
display: inline-flex;
align-items: center;
gap: 6px;
margin-top: 12px;
color: #667eea;
text-decoration: none;
font-size: 14px;
cursor: pointer;
}
.download-link:hover {
text-decoration: underline;
}
.download-link svg {
width: 18px;
height: 18px;
}
.loading {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
justify-content: center;
align-items: center;
}
.loading.show {
display: flex;
}
.loading-content {
background: #fff;
padding: 30px 50px;
border-radius: 12px;
text-align: center;
}
.spinner {
width: 40px;
height: 40px;
border: 4px solid #f3f3f3;
border-top: 4px solid #667eea;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 16px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.error-msg {
background: #fee;
color: #c33;
padding: 12px 16px;
border-radius: 8px;
margin-top: 16px;
font-size: 14px;
display: none;
}
.error-msg.show {
display: block;
}
.tips {
background: #f0f7ff;
padding: 16px;
border-radius: 10px;
margin-top: 20px;
font-size: 13px;
color: #666;
}
.tips h4 {
color: #333;
margin-bottom: 8px;
font-size: 14px;
}
.tips ul {
padding-left: 20px;
}
.tips li {
margin-bottom: 4px;
}
@media (max-width: 768px) {
.config-grid {
grid-template-columns: 1fr;
}
.btn-group {
flex-direction: column;
}
.header h1 {
font-size: 24px;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>百度TTS语音合成工具</h1>
<p>基于百度智能云短文本在线合成API</p>
</div>
<div class="main-card">
<div class="card-header">
<h2>参数配置</h2>
</div>
<div class="card-body">
<div class="form-group">
<label>合成文本 <span>必填建议不超过120字节</span></label>
<textarea
id="textInput"
class="text-input"
placeholder="请输入需要转换为语音的中文文本..."
maxlength="500"
></textarea>
<div class="char-count"><span id="charCount">0</span> / 500 字符</div>
</div>
<div class="config-grid">
<div class="config-item">
<label>发音人</label>
<select id="per">
<option value="0" selected>度小美(女声)</option>
<option value="1">度小宇(男声)</option>
<option value="3">度逍遥(情感男声)</option>
<option value="4">度丫丫(情感女声)</option>
<option value="5">度小娇(甜美女声)</option>
<option value="6">度米朵(可爱童声)</option>
<option value="7">度小文(清新女声)</option>
<option value="8">度小熊(可爱童声)</option>
<option value="9">度小霞(成熟女声)</option>
<option value="10">度小青(自然女声)</option>
<option value="11">度晓琳(知性女声)</option>
<option value="12">度晓美(温柔女声)</option>
</select>
</div>
<div class="config-item">
<label>语速</label>
<input type="range" id="spd" min="0" max="15" value="5">
<div class="range-value"><span id="spdValue">5</span></div>
</div>
<div class="config-item">
<label>音调</label>
<input type="range" id="pit" min="0" max="15" value="5">
<div class="range-value"><span id="pitValue">5</span></div>
</div>
<div class="config-item">
<label>音量</label>
<input type="range" id="vol" min="0" max="15" value="5">
<div class="range-value"><span id="volValue">5</span></div>
</div>
<div class="config-item">
<label>音频格式</label>
<select id="aue">
<option value="3" selected>MP3</option>
<option value="6">WAV</option>
<option value="4">PCM</option>
</select>
</div>
<div class="config-item">
<label>语言</label>
<select id="lan">
<option value="zh" selected>中文</option>
<option value="en">英文</option>
</select>
</div>
</div>
<div class="btn-group">
<button class="btn btn-primary" id="synthesizeBtn" onclick="synthesize()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polygon points="5 3 19 12 5 21 5 3"></polygon>
</svg>
开始合成
</button>
<button class="btn btn-secondary" onclick="resetForm()">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"></path>
<path d="M3 3v5h5"></path>
</svg>
重置
</button>
</div>
<div class="error-msg" id="errorMsg"></div>
<div class="result-section" id="resultSection">
<div class="result-header">
<h3>合成结果</h3>
<span class="file-info" id="fileInfo"></span>
</div>
<audio id="audioPlayer" class="audio-player" controls></audio>
<a class="download-link" id="downloadLink" download="">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
下载音频文件
</a>
</div>
<div class="tips">
<h4>使用提示</h4>
<ul>
<li>建议单次合成文本不超过120字节约60个汉字过长文本可能导致合成失败</li>
<li>情感发音人(度逍遥、度丫丫)可获得更自然的语音效果</li>
<li>语速范围0-15数值越大语速越快默认5为正常语速</li>
<li>音调范围0-15数值越大音调越高默认5为正常音调</li>
</ul>
</div>
</div>
</div>
</div>
<div class="loading" id="loading">
<div class="loading-content">
<div class="spinner"></div>
<p>正在合成语音...</p>
</div>
</div>
<script>
const textInput = document.getElementById('textInput');
const charCount = document.getElementById('charCount');
const spd = document.getElementById('spd');
const pit = document.getElementById('pit');
const vol = document.getElementById('vol');
const spdValue = document.getElementById('spdValue');
const pitValue = document.getElementById('pitValue');
const volValue = document.getElementById('volValue');
textInput.addEventListener('input', function() {
charCount.textContent = this.value.length;
});
spd.addEventListener('input', function() {
spdValue.textContent = this.value;
});
pit.addEventListener('input', function() {
pitValue.textContent = this.value;
});
vol.addEventListener('input', function() {
volValue.textContent = this.value;
});
function showError(message) {
const errorMsg = document.getElementById('errorMsg');
errorMsg.textContent = message;
errorMsg.classList.add('show');
setTimeout(() => {
errorMsg.classList.remove('show');
}, 5000);
}
function showLoading(show) {
document.getElementById('loading').classList.toggle('show', show);
}
function synthesize() {
const text = textInput.value.trim();
if (!text) {
showError('请输入要合成的文本内容');
return;
}
const params = {
text: text,
per: document.getElementById('per').value,
spd: spd.value,
pit: pit.value,
vol: vol.value,
aue: document.getElementById('aue').value,
lan: document.getElementById('lan').value,
cuid: 'tts_' + Date.now()
};
showLoading(true);
document.getElementById('resultSection').classList.remove('show');
document.getElementById('errorMsg').classList.remove('show');
const btn = document.getElementById('synthesizeBtn');
btn.disabled = true;
fetch('/tsf/index/synthesize', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: new URLSearchParams(params)
})
.then(response => response.json())
.then(data => {
showLoading(false);
btn.disabled = false;
if (data.code === 0 && data.data) {
const audioData = data.data;
let mimeType = 'audio/mpeg';
if (audioData.audio_format === 'wav') {
mimeType = 'audio/wav';
} else if (audioData.audio_format === 'pcm') {
mimeType = 'audio/basic';
}
const audioBlob = base64ToBlob(audioData.audio_data, mimeType);
const audioUrl = URL.createObjectURL(audioBlob);
const audioPlayer = document.getElementById('audioPlayer');
audioPlayer.src = audioUrl;
const downloadLink = document.getElementById('downloadLink');
downloadLink.href = audioUrl;
downloadLink.download = 'tts_' + Date.now() + '.' + audioData.audio_format;
const fileSize = formatFileSize(audioData.file_size);
document.getElementById('fileInfo').textContent =
`${audioData.audio_format.toUpperCase()} | ${fileSize}`;
document.getElementById('resultSection').classList.add('show');
} else {
showError(data.msg || '合成失败,请稍后重试');
}
})
.catch(error => {
showLoading(false);
btn.disabled = false;
showError('请求失败: ' + error.message);
});
}
function base64ToBlob(base64, mimeType) {
const byteCharacters = atob(base64);
const byteNumbers = new Array(byteCharacters.length);
for (let i = 0; i < byteCharacters.length; i++) {
byteNumbers[i] = byteCharacters.charCodeAt(i);
}
const byteArray = new Uint8Array(byteNumbers);
return new Blob([byteArray], { type: mimeType });
}
function formatFileSize(bytes) {
if (bytes < 1024) return bytes + ' B';
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB';
return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
}
function resetForm() {
textInput.value = '';
charCount.textContent = '0';
document.getElementById('per').value = '0';
spd.value = 5;
pit.value = 5;
vol.value = 5;
spdValue.textContent = '5';
pitValue.textContent = '5';
volValue.textContent = '5';
document.getElementById('aue').value = '3';
document.getElementById('lan').value = 'zh';
document.getElementById('resultSection').classList.remove('show');
document.getElementById('errorMsg').classList.remove('show');
}
</script>
</body>
</html>