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

JS Promise首次迭代Resolve失效:SVG动画与TTS配音同步异常

SVG路径动画与TTS配音同步异常修复

问题现象

实现SVG路径动画与微软TTS配音同步播放时,首次迭代中若语音先于动画结束,会提前进入下一轮播放;但动画先结束时功能正常。

原代码

async function playWithSound(pathArrangement, i, len) {

  const synth = window.speechSynthesis;
  pathid = pathArrangement[i];
  script = scriptData[pathData[pathid].name];

  console.log(script)

  const utterance = new SpeechSynthesisUtterance(script);
  utterance.voice = neerjaVoice || synth.getVoices()[0];

  await synth.speak(utterance);
  utterance.onstart = () => {
    document.getElementById(pathid).style.animationPlayState = 'running';
    followPencil(pathid);
  }

  // Create a promise that resolves when the speech synthesis ends
  const speechEndPromise = new Promise((resolve) => {
    utterance.onend = resolve;
  });

  const animationEndPromise = new Promise((resolve) => {
    document.getElementById(pathid).addEventListener('animationend', () => {
      resolve();
    }, {
      once: true
    });
  });
  // wait for promises to resolve and call the next stack
  await Promise.all([animationEndPromise, speechEndPromise]);
  console.log("resolved both")

  i++;
  if (i < len) {
    playWithSound(pathArrangement, i, len);
  }
  return;
}

问题根源

  1. 无意义的await:synth.speak()不是异步函数,await无法等待语音播放,导致事件绑定逻辑可能在语音开始/结束后才执行,时序错乱。
  2. 全局变量污染:pathid和script未用let/const声明,属于全局变量,递归调用时会被后续迭代覆盖,引发不可预料的元素绑定错误。
  3. 事件绑定顺序错误:utterance.onstart绑定在speak()之后,极端情况下语音已启动但事件监听未生效,导致动画未正确触发,或后续Promise逻辑异常。

修复后的代码

async function playWithSound(pathArrangement, i, len) {
  const synth = window.speechSynthesis;
  // 声明局部变量,避免全局污染
  const pathid = pathArrangement[i];
  const script = scriptData[pathData[pathid].name];

  console.log(script);

  const utterance = new SpeechSynthesisUtterance(script);
  utterance.voice = neerjaVoice || synth.getVoices()[0];

  const pathElement = document.getElementById(pathid);
  // 确保动画初始为暂停状态(如果CSS没设置的话)
  pathElement.style.animationPlayState = 'paused';

  // 先绑定所有事件,再启动语音
  const speechEndPromise = new Promise(resolve => {
    utterance.onend = resolve;
  });

  const animationEndPromise = new Promise(resolve => {
    pathElement.addEventListener('animationend', () => {
      resolve();
    }, { once: true });
  });

  utterance.onstart = () => {
    pathElement.style.animationPlayState = 'running';
    followPencil(pathid);
  };

  // 启动语音,无需await
  synth.speak(utterance);

  // 等待语音和动画都结束
  await Promise.all([animationEndPromise, speechEndPromise]);
  console.log("resolved both");

  i++;
  if (i < len) {
    playWithSound(pathArrangement, i, len);
  }
}

额外注意事项

  • 确保SVG元素的动画CSS中设置了animation-play-state: paused作为初始状态,避免动画提前运行。
  • 若neerjaVoice是异步加载的,需确保在调用playWithSound前已获取到该语音,否则会 fallback 到默认语音。

内容的提问来源于stack exchange,提问作者AwaisMehnga

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 09:57:43