公司动态
基于Web Speech API的Markdown编辑器文字转语音完整实现方案
最近在开发在线文档系统时经常遇到需要将Markdown内容转换为语音的需求比如为视障用户提供语音阅读功能或者制作音频学习资料。传统的文本转语音方案往往需要复杂的API调用和格式处理特别是对Markdown这种包含丰富格式的文档类型。本文将分享一套完整的MD编辑器文字转语音解决方案从基础概念到完整实现包含可运行的代码示例和实际项目中的优化技巧。无论你是前端开发者想要为博客添加语音功能还是需要集成到企业级文档系统中都能找到实用的实现方案。1. 文字转语音技术基础1.1 文字转语音技术概述文字转语音Text-to-Speech简称TTS是一种将书面文本转换为人类语音的技术。在现代Web开发中TTS技术已经广泛应用于无障碍访问、语音助手、在线教育等多个领域。对于MD编辑器而言TTS技术能够将Markdown格式的文档内容转换为自然流畅的语音输出大大提升了内容的可访问性。TTS技术的核心原理包括文本分析、语音合成和音频输出三个主要环节。文本分析阶段负责处理输入文本的语言特征包括分词、语法分析和韵律预测语音合成阶段根据分析结果生成对应的语音信号音频输出阶段则将合成的语音通过设备扬声器播放。1.2 Web Speech API 介绍现代浏览器提供了Web Speech API来支持文字转语音功能这是实现MD编辑器语音功能的核心技术。Web Speech API包含两个主要部分语音识别SpeechRecognition和语音合成SpeechSynthesis。对于文字转语音场景我们主要使用SpeechSynthesis接口。SpeechSynthesis API的主要特点包括支持多种语言和语音库可调节语速、音调和音量提供播放控制功能暂停、继续、停止支持语音队列管理跨浏览器兼容性较好目前主流浏览器对SpeechSynthesis API的支持情况如下Chrome 33完全支持Firefox 49完全支持Safari 7部分支持Edge 79完全支持2. 环境准备与项目搭建2.1 开发环境要求在开始实现MD编辑器文字转语音功能前需要确保开发环境满足以下要求基础环境配置现代浏览器推荐Chrome 80或Firefox 75代码编辑器VS Code、WebStorm等本地Web服务器可使用Live Server、http-server等项目技术栈HTML5页面结构CSS3样式设计JavaScript ES6核心逻辑实现可选Markdown解析库如marked.js2.2 项目结构设计建议采用以下目录结构来组织代码md-tts-project/ ├── index.html # 主页面 ├── css/ │ └── style.css # 样式文件 ├── js/ │ ├── main.js # 主逻辑 │ ├── markdown-parser.js # Markdown解析 │ └── tts-engine.js # 语音合成引擎 └── assets/ └── voices/ # 自定义语音资源可选2.3 基础HTML结构创建基础的HTML页面结构!DOCTYPE html html langzh-CN head meta charsetUTF-8 meta nameviewport contentwidthdevice-width, initial-scale1.0 titleMD编辑器文字转语音/title link relstylesheet hrefcss/style.css /head body div classcontainer header h1MD编辑器文字转语音演示/h1 /header div classeditor-container div classtoolbar button idplayBtn classbtn-primary播放语音/button button idpauseBtn classbtn-secondary暂停/button button idstopBtn classbtn-secondary停止/button select idvoiceSelect classvoice-selector option value选择语音/option /select div classcontrols label语速: input typerange idrate min0.5 max2 step0.1 value1/label label音调: input typerange idpitch min0.5 max2 step0.1 value1/label label音量: input typerange idvolume min0 max1 step0.1 value1/label /div /div div classeditor-area textarea idmarkdownInput placeholder请输入Markdown内容... # 欢迎使用MD编辑器 这是一个**示例文档**支持Markdown语法。 - 列表项1 - 列表项2 - 列表项3 代码块也会被正确处理。 /textarea div idpreview classpreview-area/div /div /div div classstatus-area div idstatus classstatus准备就绪/div div idprogress classprogress-bar/div /div /div script srcjs/markdown-parser.js/script script srcjs/tts-engine.js/script script srcjs/main.js/script /body /html3. 核心功能实现3.1 Markdown内容解析要实现MD编辑器的文字转语音首先需要将Markdown格式转换为纯文本。Markdown中的特殊符号和格式标记在语音输出时应该被适当处理。创建Markdown解析模块// js/markdown-parser.js class MarkdownParser { constructor() { this.rules { heading: /^(#{1,6})\s(.)$/gm, bold: /\*\*(.*?)\*\*/g, italic: /\*(.*?)\*/g, code: /([^])/g, link: /\[([^\]])\]\(([^)])\)/g, list: /^[\s]*[-*]\s(.)$/gm, blockquote: /^\s(.)$/gm }; } // 将Markdown转换为纯文本适合语音朗读 toPlainText(markdown) { let text markdown; // 处理标题 - 转换为普通文本并添加适当停顿 text text.replace(this.rules.heading, (match, hashes, content) { const level hashes.length; return 标题${level}级${content}。; }); // 处理粗体 - 移除标记可添加语音强调提示 text text.replace(this.rules.bold, 【强调开始】$1【强调结束】); // 处理斜体 text text.replace(this.rules.italic, $1); // 处理代码 - 特殊处理 text text.replace(this.rules.code, 代码$1); // 处理链接 text text.replace(this.rules.link, 链接$1地址$2); // 处理列表 text text.replace(this.rules.list, 列表项$1); // 处理引用 text text.replace(this.rules.blockquote, 引用$1); // 清理多余的空行和空格 text text.replace(/\n\s*\n/g, \n).trim(); return text; } // 分段处理用于长文本分段落朗读 splitIntoParagraphs(text, maxLength 500) { const paragraphs []; let currentParagraph ; const sentences text.split(/[.!?。]/); for (let sentence of sentences) { sentence sentence.trim(); if (!sentence) continue; if ((currentParagraph sentence).length maxLength) { currentParagraph (currentParagraph ? : ) sentence .; } else { if (currentParagraph) { paragraphs.push(currentParagraph); } currentParagraph sentence .; } } if (currentParagraph) { paragraphs.push(currentParagraph); } return paragraphs; } } // 导出模块 if (typeof module ! undefined module.exports) { module.exports MarkdownParser; }3.2 语音合成引擎实现创建核心的TTS引擎类封装Web Speech API的功能// js/tts-engine.js class TTSEngine { constructor() { this.synthesis window.speechSynthesis; this.utterance null; this.isSpeaking false; this.isPaused false; this.voices []; this.currentVoice null; this.rate 1.0; this.pitch 1.0; this.volume 1.0; this.onStatusChange null; this.onProgress null; this.init(); } // 初始化语音引擎 async init() { return new Promise((resolve) { // 等待语音列表加载完成 const loadVoices () { this.voices this.synthesis.getVoices(); if (this.voices.length 0) { this.setDefaultVoice(); resolve(); } }; if (this.synthesis.getVoices().length 0) { loadVoices(); } else { this.synthesis.onvoiceschanged loadVoices; } }); } // 设置默认语音优先选择中文语音 setDefaultVoice() { // 优先选择中文语音 const chineseVoices this.voices.filter(voice voice.lang.includes(zh) || voice.lang.includes(cn) ); if (chineseVoices.length 0) { this.currentVoice chineseVoices[0]; } else if (this.voices.length 0) { this.currentVoice this.voices[0]; } } // 获取可用语音列表 getVoices() { return this.voices; } // 设置语音参数 setParams({ voice null, rate null, pitch null, volume null }) { if (voice) this.currentVoice voice; if (rate ! null) this.rate Math.max(0.1, Math.min(10, rate)); if (pitch ! null) this.pitch Math.max(0, Math.min(2, pitch)); if (volume ! null) this.volume Math.max(0, Math.min(1, volume)); } // 朗读文本 speak(text, onStart null, onEnd null, onError null) { return new Promise((resolve, reject) { if (this.isSpeaking !this.isPaused) { this.stop(); } this.utterance new SpeechSynthesisUtterance(text); // 设置语音参数 if (this.currentVoice) { this.utterance.voice this.currentVoice; } this.utterance.rate this.rate; this.utterance.pitch this.pitch; this.utterance.volume this.volume; this.utterance.lang this.currentVoice ? this.currentVoice.lang : zh-CN; // 事件处理 this.utterance.onstart () { this.isSpeaking true; this.isPaused false; if (onStart) onStart(); if (this.onStatusChange) this.onStatusChange(speaking); }; this.utterance.onend () { this.isSpeaking false; this.isPaused false; if (onEnd) onEnd(); if (this.onStatusChange) this.onStatusChange(ended); resolve(); }; this.utterance.onerror (event) { this.isSpeaking false; this.isPaused false; if (onError) onError(event.error); if (this.onStatusChange) this.onStatusChange(error); reject(event.error); }; this.utterance.onpause () { this.isPaused true; if (this.onStatusChange) this.onStatusChange(paused); }; this.utterance.onresume () { this.isPaused false; if (this.onStatusChange) this.onStatusChange(resumed); }; this.utterance.onboundary (event) { if (this.onProgress) { this.onProgress({ charIndex: event.charIndex, charLength: event.charLength, name: event.name }); } }; this.synthesis.speak(this.utterance); }); } // 暂停朗读 pause() { if (this.isSpeaking !this.isPaused) { this.synthesis.pause(); this.isPaused true; } } // 继续朗读 resume() { if (this.isSpeaking this.isPaused) { this.synthesis.resume(); this.isPaused false; } } // 停止朗读 stop() { if (this.isSpeaking) { this.synthesis.cancel(); this.isSpeaking false; this.isPaused false; if (this.onStatusChange) this.onStatusChange(stopped); } } // 获取当前状态 getStatus() { return { isSpeaking: this.isSpeaking, isPaused: this.isPaused, currentVoice: this.currentVoice, rate: this.rate, pitch: this.pitch, volume: this.volume }; } }3.3 主控制逻辑集成创建主控制模块整合Markdown解析和TTS功能// js/main.js class MDTTSController { constructor() { this.parser new MarkdownParser(); this.ttsEngine new TTSEngine(); this.isProcessing false; this.currentText ; this.initElements(); this.bindEvents(); this.initTTS(); } // 初始化DOM元素引用 initElements() { this.elements { markdownInput: document.getElementById(markdownInput), preview: document.getElementById(preview), playBtn: document.getElementById(playBtn), pauseBtn: document.getElementById(pauseBtn), stopBtn: document.getElementById(stopBtn), voiceSelect: document.getElementById(voiceSelect), rate: document.getElementById(rate), pitch: document.getElementById(pitch), volume: document.getElementById(volume), status: document.getElementById(status), progress: document.getElementById(progress) }; } // 绑定事件处理 bindEvents() { this.elements.playBtn.addEventListener(click, () this.playTTS()); this.elements.pauseBtn.addEventListener(click, () this.pauseTTS()); this.elements.stopBtn.addEventListener(click, () this.stopTTS()); this.elements.voiceSelect.addEventListener(change, (e) this.changeVoice(e)); this.elements.rate.addEventListener(input, (e) this.changeRate(e)); this.elements.pitch.addEventListener(input, (e) this.changePitch(e)); this.elements.volume.addEventListener(input, (e) this.changeVolume(e)); this.elements.markdownInput.addEventListener(input, () this.updatePreview()); } // 初始化TTS引擎 async initTTS() { try { await this.ttsEngine.init(); this.populateVoiceList(); this.updateStatus(TTS引擎初始化完成); } catch (error) { this.updateStatus(TTS引擎初始化失败: error.message); } } // 填充语音选择列表 populateVoiceList() { const voices this.ttsEngine.getVoices(); const select this.elements.voiceSelect; select.innerHTML option value选择语音/option; voices.forEach(voice { const option document.createElement(option); option.value voice.name; option.textContent ${voice.name} (${voice.lang}); option.dataset.voice JSON.stringify(voice); select.appendChild(option); }); } // 播放TTS async playTTS() { if (this.isProcessing) return; try { this.isProcessing true; this.updateStatus(正在处理Markdown内容...); const markdown this.elements.markdownInput.value; if (!markdown.trim()) { this.updateStatus(请输入Markdown内容); this.isProcessing false; return; } // 转换Markdown为纯文本 this.currentText this.parser.toPlainText(markdown); this.updateStatus(开始朗读...); await this.ttsEngine.speak( this.currentText, () this.updateStatus(朗读中...), () { this.updateStatus(朗读完成); this.isProcessing false; }, (error) { this.updateStatus(朗读错误: error); this.isProcessing false; } ); } catch (error) { this.updateStatus(播放失败: error.message); this.isProcessing false; } } // 暂停TTS pauseTTS() { this.ttsEngine.pause(); this.updateStatus(已暂停); } // 停止TTS stopTTS() { this.ttsEngine.stop(); this.updateStatus(已停止); this.isProcessing false; } // 切换语音 changeVoice(event) { const selectedOption event.target.selectedOptions[0]; if (selectedOption selectedOption.dataset.voice) { const voice JSON.parse(selectedOption.dataset.voice); this.ttsEngine.setParams({ voice }); this.updateStatus(已切换到语音: ${voice.name}); } } // 调整语速 changeRate(event) { const rate parseFloat(event.target.value); this.ttsEngine.setParams({ rate }); this.elements.rate.nextElementSibling.textContent rate.toFixed(1); } // 调整音调 changePitch(event) { const pitch parseFloat(event.target.value); this.ttsEngine.setParams({ pitch }); this.elements.pitch.nextElementSibling.textContent pitch.toFixed(1); } // 调整音量 changeVolume(event) { const volume parseFloat(event.target.value); this.ttsEngine.setParams({ volume }); this.elements.volume.nextElementSibling.textContent volume.toFixed(1); } // 更新预览 updatePreview() { // 这里可以添加Markdown实时预览功能 // 由于重点是TTS这里简单显示纯文本预览 const markdown this.elements.markdownInput.value; const plainText this.parser.toPlainText(markdown); this.elements.preview.textContent plainText; } // 更新状态显示 updateStatus(message) { this.elements.status.textContent message; console.log(Status:, message); } } // 页面加载完成后初始化 document.addEventListener(DOMContentLoaded, () { new MDTTSController(); });4. 样式设计与用户体验优化4.1 基础样式设计创建CSS样式文件确保界面美观且易用/* css/style.css */ * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: Segoe UI, Tahoma, Geneva, Verdana, sans-serif; line-height: 1.6; color: #333; background-color: #f5f5f5; } .container { max-width: 1200px; margin: 0 auto; padding: 20px; } header { text-align: center; margin-bottom: 30px; } header h1 { color: #2c3e50; font-size: 2.5rem; margin-bottom: 10px; } .editor-container { background: white; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); overflow: hidden; } .toolbar { background: #34495e; padding: 15px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; } .btn-primary, .btn-secondary { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; font-size: 14px; transition: background-color 0.3s; } .btn-primary { background: #3498db; color: white; } .btn-primary:hover { background: #2980b9; } .btn-secondary { background: #95a5a6; color: white; } .btn-secondary:hover { background: #7f8c8d; } .btn-secondary:disabled { background: #bdc3c7; cursor: not-allowed; } .voice-selector { padding: 8px; border: 1px solid #ddd; border-radius: 4px; background: white; min-width: 200px; } .controls { display: flex; gap: 15px; align-items: center; margin-left: auto; } .controls label { color: white; font-size: 14px; display: flex; align-items: center; gap: 5px; } .controls input[typerange] { width: 80px; } .editor-area { display: grid; grid-template-columns: 1fr 1fr; gap: 0; height: 400px; } #markdownInput { width: 100%; height: 100%; padding: 20px; border: none; border-right: 1px solid #eee; font-family: Courier New, monospace; font-size: 14px; resize: none; outline: none; } .preview-area { padding: 20px; overflow-y: auto; background: #fafafa; border-left: 1px solid #eee; } .status-area { margin-top: 20px; background: white; padding: 15px; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .status { font-weight: bold; color: #2c3e50; margin-bottom: 10px; } .progress-bar { height: 4px; background: #ecf0f1; border-radius: 2px; overflow: hidden; } .progress-bar::after { content: ; display: block; height: 100%; background: #3498db; width: 0%; transition: width 0.3s; } .progress-bar.active::after { width: 100%; animation: progress 2s linear infinite; } keyframes progress { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } } /* 响应式设计 */ media (max-width: 768px) { .editor-area { grid-template-columns: 1fr; height: 600px; } #markdownInput { border-right: none; border-bottom: 1px solid #eee; } .toolbar { flex-direction: column; align-items: stretch; } .controls { margin-left: 0; justify-content: space-between; } }4.2 交互优化功能增强用户体验的交互功能// 在main.js的MDTTSController类中添加以下方法 class MDTTSController { // ... 之前的代码 ... // 添加进度显示功能 setupProgressTracking() { this.ttsEngine.onProgress (progress) { this.updateProgressBar(progress); }; } updateProgressBar(progress) { const progressBar this.elements.progress; if (progress.charLength 0) { const percentage (progress.charIndex / progress.charLength) * 100; progressBar.style.setProperty(--progress, ${percentage}%); } } // 添加键盘快捷键支持 setupKeyboardShortcuts() { document.addEventListener(keydown, (event) { if (event.ctrlKey || event.metaKey) { switch (event.key) { case Enter: event.preventDefault(); this.playTTS(); break; case : event.preventDefault(); if (this.ttsEngine.getStatus().isSpeaking) { if (this.ttsEngine.getStatus().isPaused) { this.ttsEngine.resume(); } else { this.ttsEngine.pause(); } } break; case Escape: event.preventDefault(); this.stopTTS(); break; } } }); } // 添加语音合成状态指示 setupStatusIndicators() { this.ttsEngine.onStatusChange (status) { const statusElement this.elements.status; const progressBar this.elements.progress; switch (status) { case speaking: statusElement.style.color #27ae60; progressBar.classList.add(active); break; case paused: statusElement.style.color #f39c12; progressBar.classList.remove(active); break; case stopped: case ended: statusElement.style.color #2c3e50; progressBar.classList.remove(active); progressBar.style.setProperty(--progress, 0%); break; case error: statusElement.style.color #e74c3c; progressBar.classList.remove(active); break; } }; } }5. 高级功能扩展5.1 批量处理与队列管理对于长文档需要实现分段朗读和队列管理// 扩展TTSEngine类添加队列功能 class AdvancedTTSEngine extends TTSEngine { constructor() { super(); this.queue []; this.isProcessingQueue false; this.currentQueueItem null; } // 添加到队列 addToQueue(text, priority false) { const queueItem { text, id: Date.now() Math.random(), priority }; if (priority) { this.queue.unshift(queueItem); } else { this.queue.push(queueItem); } if (!this.isProcessingQueue) { this.processQueue(); } return queueItem.id; } // 处理队列 async processQueue() { if (this.isProcessingQueue || this.queue.length 0) return; this.isProcessingQueue true; while (this.queue.length 0) { this.currentQueueItem this.queue.shift(); try { await this.speak(this.currentQueueItem.text); } catch (error) { console.error(队列项目朗读失败:, error); } this.currentQueueItem null; } this.isProcessingQueue false; } // 清空队列 clearQueue() { this.queue []; if (this.currentQueueItem) { this.stop(); } } // 获取队列信息 getQueueInfo() { return { total: this.queue.length, current: this.currentQueueItem, isProcessing: this.isProcessingQueue }; } }5.2 自定义语音样式处理针对不同的Markdown元素实现不同的语音表现// 扩展MarkdownParser类支持更精细的语音处理 class AdvancedMarkdownParser extends MarkdownParser { constructor() { super(); this.voiceStyles { heading: { rate: 0.9, pitch: 1.1 }, emphasis: { rate: 1.0, pitch: 1.2 }, code: { rate: 1.1, pitch: 0.9 }, normal: { rate: 1.0, pitch: 1.0 } }; } // 分段处理并标记语音样式 parseWithVoiceStyles(markdown) { const segments []; let remainingText markdown; // 处理标题 remainingText remainingText.replace(this.rules.heading, (match, hashes, content) { segments.push({ text: 标题${hashes.length}级${content}, style: this.voiceStyles.heading }); return ; }); // 处理强调文本 remainingText remainingText.replace(this.rules.bold, (match, content) { const before remainingText.substring(0, remainingText.indexOf(match)); if (before) { segments.push({ text: before, style: this.voiceStyles.normal }); } segments.push({ text: content, style: this.voiceStyles.emphasis }); remainingText remainingText.substring(remainingText.indexOf(match) match.length); return ; }); // 处理剩余普通文本 if (remainingText.trim()) { const paragraphs remainingText.split(\n\n); paragraphs.forEach(paragraph { if (paragraph.trim()) { segments.push({ text: paragraph, style: this.voiceStyles.normal }); } }); } return segments; } }6. 常见问题与解决方案6.1 浏览器兼容性问题问题1SpeechSynthesis API不被支持解决方案添加特性检测和降级方案// 特性检测 if (!(speechSynthesis in window)) { alert(您的浏览器不支持语音合成功能请使用Chrome、Firefox或Edge等现代浏览器。); // 可以在这里提供替代方案如音频文件播放 } // 降级方案实现 class FallbackTTS { constructor() { this.audioContext null; this.isSupported false; this.checkSupport(); } checkSupport() { this.isSupported speechSynthesis in window; if (!this.isSupported) { this.setupAudioFallback(); } } setupAudioFallback() { // 这里可以实现基于预录制音频的降级方案 console.warn(使用降级语音方案); } }问题2语音列表加载延迟解决方案实现语音加载等待机制// 改进的语音加载方法 async waitForVoices(timeout 5000) { return new Promise((resolve, reject) { const voices speechSynthesis.getVoices(); if (voices.length 0) { resolve(voices); return; } const timer setTimeout(() { speechSynthesis.onvoiceschanged null; reject(new Error(语音加载超时)); }, timeout); speechSynthesis.onvoiceschanged () { clearTimeout(timer); const loadedVoices speechSynthesis.getVoices(); if (loadedVoices.length 0) { resolve(loadedVoices); } else { reject(new Error(未找到可用语音)); } }; }); }6.2 性能优化问题问题3长文本内存占用过高解决方案实现流式处理和分段朗读// 流式处理长文本 class StreamTTS { constructor() { this.chunkSize 1000; // 每次处理1000字符 } async speakLongText(text, onChunkStart null) { const chunks this.splitTextIntoChunks(text); for (let i 0; i chunks.length; i) { if (onChunkStart) { onChunkStart(i, chunks.length); } await this.speakChunk(chunks[i]); } } splitTextIntoChunks(text) { const chunks []; let start 0; while (start text.length) { let end start this.chunkSize; if (end text.length) { chunks.push(text.substring(start)); break; } // 在句子边界处分割 const lastPeriod text.lastIndexOf(., end); const lastQuestion text.lastIndexOf(?, end); const lastExclamation text.lastIndexOf(!, end); const lastBreak Math.max(lastPeriod, lastQuestion, lastExclamation); if (lastBreak start) { end lastBreak 1; } chunks.push(text.substring(start, end)); start end; } return chunks; } async speakChunk(chunk) { return new Promise((resolve) { const utterance new SpeechSynthesisUtterance(chunk); utterance.onend resolve; utterance.onerror resolve; speechSynthesis.speak(utterance); }); } }6.3 用户体验问题问题4语音中断处理解决方案实现智能中断恢复机制// 智能中断处理 class SmartTTSInterrupt { constructor() { this.lastPosition 0; this.currentText ; } // 保存朗读位置 savePosition(text, charIndex) { this.lastPosition charIndex; this.currentText text; } // 从断点继续 continueFromBreak() { if (this.lastPosition 0 this.currentText) { const remainingText this.currentText.substring(this.lastPosition); return remainingText; } return null; } // 处理用户中断 handleInterrupt() { this.savePosition(this.currentText, this.getCurrentCharIndex()); } getCurrentCharIndex() { // 这里需要根据实际情况获取当前朗读位置 return this.lastPosition; } }7. 生产环境最佳实践7.1 错误处理与日志记录在生产环境中完善的错误处理至关重要// 生产级错误处理 class ProductionTTSEngine { constructor() { this.errorCount 0; this.maxErrors 3; this.logger this.setupLogger(); } setupLogger() { return { info: (message, data) console.log([INFO] ${message}, data), warn: (message, data) console.warn([WARN] ${message}, data), error: (message, error) { console.error([ERROR] ${message}, error); this.errorCount; if (this.errorCount this.maxErrors) { this.disableTTS(); } } }; } async speakWithRetry(text, retries 3) { for (let attempt 1; attempt retries; attempt) { try { await this.speak(text); this.errorCount 0; // 重置错误计数 return; } catch (error) { this.logger.error(朗读尝试 ${attempt} 失败, error); if (attempt retries