如何优化speak.js加载处理速度,解决大文本无法加载问题?
问题
在小说网站使用speak.js实现文本转语音(TTS)功能时,短文本表现正常,但处理约1000字内容时加载耗时3-5分钟,10000字及以上内容直接无法加载。目前仅通过页面脚本实现,未修改核心代码,寻求优化加载速度与解决大文本加载失败的方案。
现状代码
客户端代码
var speakWorker; try { speakWorker = new Worker('/speakWorker'); } catch(e) { console.log('speak.js warning: no worker support'); } function speak(text, args) { var PROFILE = 1; function parseWav(wav) { function readInt(i, bytes) { var ret = 0; var shft = 0; while (bytes) { ret += wav[i] << shft; shft += 8; i++; bytes--; } return ret; } if (readInt(20, 2) != 1) throw 'Invalid compression code, not PCM'; if (readInt(22, 2) != 1) throw 'Invalid number of channels, not 1'; return { sampleRate: readInt(24, 4), bitsPerSample: readInt(34, 2), samples: wav.subarray(44) }; } function playHTMLAudioElement(wav) { function encode64(data) { var BASE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; var PAD = '='; var ret = ''; var leftchar = 0; var leftbits = 0; for (var i = 0; i < data.length; i++) { leftchar = (leftchar << 8) | data[i]; leftbits += 8; while (leftbits >= 6) { var curr = (leftchar >> (leftbits-6)) & 0x3f; leftbits -= 6; ret += BASE[curr]; } } if (leftbits == 2) { ret += BASE[(leftchar&3) << 4]; ret += PAD + PAD; } else if (leftbits == 4) { ret += BASE[(leftchar&0xf) << 2]; ret += PAD; } return ret; } document.getElementById("audio").innerHTML=("<audio id=\"player\" src=\"data:audio/x-wav;base64,"+encode64(wav)+"\">"); document.getElementById("player").play(); } function playAudioDataAPI(data) { try { var output = new Audio(); output.mozSetup(1, data.sampleRate); var num = data.samples.length; var buffer = data.samples; var f32Buffer = new Float32Array(num); for (var i = 0; i < num; i++) { var value = buffer[i<<1] + (buffer[(i<<1)+1]<<8); if (value >= 0x8000) value |= ~0x7FFF; f32Buffer[i] = value / 0x8000; } output.mozWriteAudio(f32Buffer); return true; } catch(e) { return false; } } function handleWav(wav) { var startTime = Date.now(); var data = parseWav(wav); // validate the data and parse it // TODO: try playAudioDataAPI(data), and fallback if failed playHTMLAudioElement(wav); if (PROFILE) console.log('speak.js: wav processing took ' + (Date.now()-startTime).toFixed(2) + ' ms'); } if (args && args.noWorker) { // Do everything right now. speakGenerator.js must have been loaded. var startTime = Date.now(); var wav = generateSpeech(text, args); if (PROFILE) console.log('speak.js: processing took ' + (Date.now()-startTime).toFixed(2) + ' ms'); handleWav(wav); } else { // Call the worker, which will return a wav that we then play var startTime = Date.now(); speakWorker.onmessage = function(event) { if (PROFILE) console.log('speak.js: worker processing took ' + (Date.now()-startTime).toFixed(2) + ' ms'); handleWav(event.data); }; speakWorker.postMessage({ text: text, args: args }); } }
Worker代码
importScripts('/speakGenerator'); onmessage = function(event) { postMessage(generateSpeech(event.data.text, event.data.args)); };
页面代码
<script src="/speakClient"></script> <div class="flex"> <button id="tts" class="speakButton"><i class="fa-solid fa-volume"></i></button> </div> <div id="audio"></div> <script type="text/javascript"> (async () => { try { let response = await fetch($$data$$); let json = await response.json(); let index = Number("$$previous$$"); if (json.type === "Novel") { let novelContent = json.chapters[index].novel; console.log(novelContent) function speakOnClick() { speak(novelContent, { amplitude: 100, wordgap: 5, pitch: 50, speed: 175 }); } // Add an event listener for 'click' event document.getElementById('tts').addEventListener('click', speakOnClick); } } catch (err) { console.log(err); } })(); </script>
优化方案
1. 分块处理文本,流式生成与播放
将大文本分割为200-300字的段落/短句,逐块生成音频并连续播放,避免一次性处理超大文本导致的内存溢出和长时间等待:
- 修改Worker支持接收单块文本,处理完成后立即返回音频数据
- 客户端维护播放队列,当前块播放结束后自动请求下一块
- 示例:在页面代码中添加文本分割函数,替换原播放逻辑为流式播放
2. 替换Base64编码为Blob URL
原代码用Base64生成data URL,大音频会导致字符串过长、内存占用过高。改用Blob URL优化性能:
function playHTMLAudioElement(wav) { const blob = new Blob([wav], { type: 'audio/wav' }); const audioUrl = URL.createObjectURL(blob); const audioElement = document.createElement('audio'); audioElement.src = audioUrl; audioElement.play(); // 播放结束后释放URL,避免内存泄漏 audioElement.onended = () => URL.revokeObjectURL(audioUrl); const audioContainer = document.getElementById("audio"); audioContainer.innerHTML = ''; audioContainer.appendChild(audioElement); }
3. 优化Worker通信与内存管理
- 避免一次性传递超大文本到Worker,分块传递减少序列化开销
- Worker处理完单块文本后,主动清理临时变量,避免内存累积
- 切换文本时终止旧Worker并创建新实例,释放闲置资源
4. 改用标准Web Audio API播放
替换老旧的mozWriteAudio实现,使用兼容性更好、性能更优的标准Web Audio API:
function playWithWebAudio(data) { const audioContext = new (window.AudioContext || window.webkitAudioContext)(); const buffer = audioContext.createBuffer(1, data.samples.length / 2, data.sampleRate); const channelData = buffer.getChannelData(0); // 转换PCM数据到标准浮点数格式 for (let i = 0; i < data.samples.length; i += 2) { let sample = (data.samples[i] | (data.samples[i+1] << 8)); if (sample >= 0x8000) sample |= ~0x7FFF; channelData[i/2] = sample / 0x8000; } const source = audioContext.createBufferSource(); source.buffer = buffer; source.connect(audioContext.destination); source.start(); return true; }
在handleWav中优先调用此函数,失败后再回退到audio元素播放。
5. 文本分块与流式播放示例实现
// 文本分割函数 function splitTextIntoChunks(text, chunkSize = 300) { const sentences = text.split(/[。!?;]/); const chunks = []; let currentChunk = ''; for (const sentence of sentences) { if (currentChunk.length + sentence.length > chunkSize) { chunks.push(currentChunk.trim()); currentChunk = sentence + '。'; } else { currentChunk += sentence + '。'; } } if (currentChunk) chunks.push(currentChunk.trim()); return chunks; } // 流式播放逻辑 function playChunksSequentially(chunks, args) { let currentIndex = 0; function playNext() { if (currentIndex >= chunks.length) return; const chunk = chunks[currentIndex]; speakWorker.onmessage = function(event) { const wav = event.data; const blob = new Blob([wav], { type: 'audio/wav' }); const audioUrl = URL.createObjectURL(blob); const audioElement = new Audio(audioUrl); audioElement.onended = () => { URL.revokeObjectURL(audioUrl); currentIndex++; playNext(); }; audioElement.play(); const container = document.getElementById("audio"); container.innerHTML = ''; container.appendChild(audioElement); }; speakWorker.postMessage({ text: chunk, args: args }); } playNext(); } // 修改原点击事件 function speakOnClick() { const textChunks = splitTextIntoChunks(novelContent); playChunksSequentially(textChunks, { amplitude: 100, wordgap: 5, pitch: 50, speed: 175 }); }
内容的提问来源于stack exchange,提问作者Jesus Perez
相关产品推荐
相关产品推荐

