xinli/局域网TTS工具.html

553 lines
16 KiB
HTML
Raw Normal View History

2025-11-26 14:14:19 +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>
<!-- 引入Element UI样式 -->
<link rel="stylesheet" href="https://unpkg.com/element-ui/lib/theme-chalk/index.css">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 12px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
}
.header h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.header p {
color: #666;
font-size: 14px;
}
.input-section {
margin-bottom: 30px;
}
.input-section label {
display: block;
margin-bottom: 8px;
color: #333;
font-weight: 500;
}
.input-section textarea {
width: 100%;
min-height: 120px;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 6px;
font-size: 14px;
resize: vertical;
transition: border-color 0.3s;
}
.input-section textarea:focus {
outline: none;
border-color: #667eea;
}
.preset-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-top: 10px;
}
.preset-btn {
padding: 8px 16px;
background: #f0f0f0;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s;
font-size: 13px;
}
.preset-btn:hover {
background: #667eea;
color: white;
border-color: #667eea;
}
.control-section {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.control-item {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
}
.control-item label {
display: block;
margin-bottom: 10px;
color: #333;
font-weight: 500;
}
.control-item .slider-container {
display: flex;
align-items: center;
gap: 15px;
}
.slider {
flex: 1;
height: 6px;
background: #e0e0e0;
border-radius: 3px;
position: relative;
cursor: pointer;
}
.slider-track {
height: 100%;
background: #667eea;
border-radius: 3px;
transition: width 0.2s;
}
.slider-thumb {
width: 18px;
height: 18px;
background: white;
border: 2px solid #667eea;
border-radius: 50%;
position: absolute;
top: 50%;
transform: translate(-50%, -50%);
cursor: grab;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
}
.slider-thumb:active {
cursor: grabbing;
}
.value-display {
min-width: 60px;
text-align: right;
color: #666;
font-size: 14px;
}
.button-group {
display: flex;
gap: 15px;
justify-content: center;
margin-top: 30px;
}
.btn {
padding: 12px 30px;
border: none;
border-radius: 6px;
font-size: 16px;
cursor: pointer;
transition: all 0.3s;
font-weight: 500;
}
.btn-primary {
background: #667eea;
color: white;
}
.btn-primary:hover:not(:disabled) {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-danger {
background: #f56565;
color: white;
}
.btn-danger:hover:not(:disabled) {
background: #e53e3e;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(245, 101, 101, 0.4);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.status {
text-align: center;
margin-top: 20px;
padding: 15px;
border-radius: 6px;
font-size: 14px;
}
.status.info {
background: #e3f2fd;
color: #1976d2;
}
.status.warning {
background: #fff3e0;
color: #f57c00;
}
.status.success {
background: #e8f5e9;
color: #388e3c;
}
.status.error {
background: #ffebee;
color: #d32f2f;
}
@media (max-width: 768px) {
.container {
padding: 20px;
}
.control-section {
grid-template-columns: 1fr;
}
.button-group {
flex-direction: column;
}
.btn {
width: 100%;
}
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎤 局域网文字转语音工具</h1>
<p>基于浏览器 Web Speech API无需后端服务纯前端实现</p>
</div>
<div class="input-section">
<label for="textInput">📝 输入要朗读的文字:</label>
<textarea id="textInput" placeholder="请输入要转换为语音的文字内容..."></textarea>
<div class="preset-buttons">
<button class="preset-btn" onclick="setPresetText('欢迎使用AI心理健康测评系统请仔细阅读题目并选择您的答案。')">
欢迎语
</button>
<button class="preset-btn" onclick="setPresetText('请根据您的实际情况,选择最符合的选项。')">
答题提示
</button>
<button class="preset-btn" onclick="setPresetText('感谢您的参与,测评已完成,请查看您的测评报告。')">
完成提示
</button>
<button class="preset-btn" onclick="setPresetText('请注意,本次测评结果仅供参考,如有疑问请咨询专业心理医生。')">
免责声明
</button>
</div>
</div>
<div class="control-section">
<div class="control-item">
<label>🔊 音量:<span id="volumeValue">100%</span></label>
<div class="slider-container">
<div class="slider" id="volumeSlider">
<div class="slider-track" id="volumeTrack"></div>
<div class="slider-thumb" id="volumeThumb"></div>
</div>
</div>
</div>
<div class="control-item">
<label>⚡ 语速:<span id="rateValue">1.0x</span></label>
<div class="slider-container">
<div class="slider" id="rateSlider">
<div class="slider-track" id="rateTrack"></div>
<div class="slider-thumb" id="rateThumb"></div>
</div>
</div>
</div>
<div class="control-item">
<label>🎵 音调:<span id="pitchValue">1.0</span></label>
<div class="slider-container">
<div class="slider" id="pitchSlider">
<div class="slider-track" id="pitchTrack"></div>
<div class="slider-thumb" id="pitchThumb"></div>
</div>
</div>
</div>
</div>
<div class="button-group">
<button class="btn btn-primary" id="speakBtn" onclick="speakText()">
▶️ 开始朗读
</button>
<button class="btn btn-danger" id="stopBtn" onclick="stopSpeaking()" disabled>
⏹️ 停止朗读
</button>
<button class="btn btn-primary" onclick="clearText()">
🗑️ 清空文本
</button>
</div>
<div class="status info" id="status">
💡 提示:请在 Chrome、Edge 或 Safari 浏览器中使用以获得最佳体验
</div>
</div>
<script>
// 全局变量
let synth = null;
let utterance = null;
let isSpeaking = false;
// 参数值
let volume = 1.0; // 0-1
let rate = 1.0; // 0.5-2
let pitch = 1.0; // 0.5-2
// 初始化
window.onload = function() {
initTts();
initSliders();
};
/**
* 初始化 TTS
*/
function initTts() {
if ('speechSynthesis' in window) {
synth = window.speechSynthesis;
updateStatus('success', '✅ 语音合成功能已就绪');
} else {
updateStatus('error', '❌ 您的浏览器不支持语音合成功能,请使用 Chrome、Edge 或 Safari 浏览器');
document.getElementById('speakBtn').disabled = true;
}
}
/**
* 初始化滑块
*/
function initSliders() {
initSlider('volume', 0, 1, 1.0, updateVolume);
initSlider('rate', 0.5, 2, 1.0, updateRate);
initSlider('pitch', 0.5, 2, 1.0, updatePitch);
}
/**
* 初始化单个滑块
*/
function initSlider(name, min, max, value, callback) {
const slider = document.getElementById(name + 'Slider');
const thumb = document.getElementById(name + 'Thumb');
const track = document.getElementById(name + 'Track');
const valueDisplay = document.getElementById(name + 'Value');
let isDragging = false;
function updateSlider(clientX) {
const rect = slider.getBoundingClientRect();
let percent = (clientX - rect.left) / rect.width;
percent = Math.max(0, Math.min(1, percent));
const newValue = min + percent * (max - min);
const displayValue = name === 'volume'
? Math.round(newValue * 100) + '%'
: name === 'rate'
? newValue.toFixed(1) + 'x'
: newValue.toFixed(1);
valueDisplay.textContent = displayValue;
thumb.style.left = percent * 100 + '%';
track.style.width = percent * 100 + '%';
callback(newValue);
}
slider.addEventListener('mousedown', (e) => {
isDragging = true;
updateSlider(e.clientX);
});
document.addEventListener('mousemove', (e) => {
if (isDragging) {
updateSlider(e.clientX);
}
});
document.addEventListener('mouseup', () => {
isDragging = false;
});
// 初始化位置
const percent = (value - min) / (max - min);
thumb.style.left = percent * 100 + '%';
track.style.width = percent * 100 + '%';
}
/**
* 更新音量
*/
function updateVolume(value) {
volume = value;
if (utterance) {
utterance.volume = value;
}
}
/**
* 更新语速
*/
function updateRate(value) {
rate = value;
if (utterance) {
utterance.rate = value;
}
}
/**
* 更新音调
*/
function updatePitch(value) {
pitch = value;
if (utterance) {
utterance.pitch = value;
}
}
/**
* 朗读文本
*/
function speakText() {
const text = document.getElementById('textInput').value.trim();
if (!text) {
updateStatus('warning', '⚠️ 请输入要朗读的文字');
return;
}
if (!synth) {
updateStatus('error', '❌ 语音合成功能不可用');
return;
}
// 停止之前的朗读
stopSpeaking();
// 创建语音合成对象
utterance = new SpeechSynthesisUtterance(text);
// 设置参数
utterance.lang = 'zh-CN';
utterance.volume = volume;
utterance.rate = rate;
utterance.pitch = pitch;
// 选择中文语音
const voices = synth.getVoices();
const chineseVoice = voices.find(voice =>
voice.lang.includes('zh') || voice.lang.includes('CN')
);
if (chineseVoice) {
utterance.voice = chineseVoice;
}
// 监听事件
utterance.onstart = () => {
isSpeaking = true;
document.getElementById('speakBtn').disabled = true;
document.getElementById('stopBtn').disabled = false;
updateStatus('success', '🔊 正在朗读...');
};
utterance.onend = () => {
isSpeaking = false;
document.getElementById('speakBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
updateStatus('info', '✅ 朗读完成');
};
utterance.onerror = (event) => {
isSpeaking = false;
document.getElementById('speakBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
updateStatus('error', '❌ 朗读失败: ' + event.error);
};
// 开始朗读
synth.speak(utterance);
}
/**
* 停止朗读
*/
function stopSpeaking() {
if (synth && synth.speaking) {
synth.cancel();
isSpeaking = false;
document.getElementById('speakBtn').disabled = false;
document.getElementById('stopBtn').disabled = true;
updateStatus('info', '⏹️ 已停止朗读');
}
}
/**
* 清空文本
*/
function clearText() {
document.getElementById('textInput').value = '';
stopSpeaking();
}
/**
* 设置预设文本
*/
function setPresetText(text) {
document.getElementById('textInput').value = text;
}
/**
* 更新状态提示
*/
function updateStatus(type, message) {
const statusEl = document.getElementById('status');
statusEl.className = 'status ' + type;
statusEl.textContent = message;
}
</script>
</body>
</html>