xinli/局域网TTS工具.html
2025-11-26 14:14:19 +08:00

553 lines
16 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>
<!-- 引入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>