You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.01 20:45:56