
本文介绍在流式文本转语音场景中,如何在不阻塞主线程的前提下,实时响应中断信号并终止后续 promise 执行,避免“幽灵调用”问题。核心方案是结合 abortcontroller、promise.race 和可取消的异步函数。
本文介绍在流式文本转语音场景中,如何在不阻塞主线程的前提下,实时响应中断信号并终止后续 promise 执行,避免“幽灵调用”问题。核心方案是结合 abortcontroller、promise.race 和可取消的异步函数。
在流式 AI 响应(如 OpenAI 的 stream: true)中逐句合成语音时,一个常见却棘手的问题是:用户中途触发中断(如点击停止按钮),虽然 for await 循环能及时 break,但已发起的 await send_voice(...) 仍会继续执行——因为 Promise 一旦 .then() 或 await 启动,便无法原生取消。这会导致冗余音频播放、资源浪费甚至状态错乱。
根本解法不是“阻止 await”,而是让每个异步操作自身具备可取消性。现代浏览器和主流库(如 fetch、Axios)均支持 AbortController,而自定义异步函数也可通过 Promise.race + 中断信号实现优雅退出:
// ✅ 可取消的 send_voice 封装
async function send_voice_cancelable(
sentence,
ws,
elleven_key,
voice_id,
interruptionState,
signal // AbortSignal 实例
) {
// 检查初始中断状态
if (interruptionState.value) {
throw new Error("Operation aborted by user");
}
// race:在中断发生时提前 reject
return Promise.race([
send_voice(sentence, ws, elleven_key, voice_id, interruptionState),
new Promise((_, reject) => {
const check = () => {
if (interruptionState.value) {
reject(new Error("Interrupted before completion"));
} else {
requestIdleCallback(check, { timeout: 50 }); // 轻量轮询
}
};
check();
})
]);
}
// ✅ 主流程:使用 AbortController 管理生命周期
const controller = new AbortController();
const { signal } = controller;
let sentence = "";
try {
const stream = await openai.chat.completions.create({
model: "gpt-3.5-turbo",
messages: conversation_history,
stream: true,
signal // 透传给 fetch(若 SDK 支持)
});
for await (const chunk of stream) {
if (signal.aborted || interruptionState.value) {
console.log("Stream interrupted — exiting loop");
break;
}
const content = chunk.choices[0]?.delta?.content || "";
sentence += content;
if (['.', '!', '?'].some(ending => sentence.endsWith(ending))) {
console.log("FINAL:", sentence);
conversation_history.push({ role: "system", content: sentence });
// 关键:传入 signal 并 await 可取消版本
await send_voice_cancelable(
sentence,
ws,
elleven_key,
voice_id,
interruptionState,
signal
);
sentence = "";
}
}
} catch (err) {
if (err.name === "AbortError" || err.message.includes("Interrupted") || err.message.includes("aborted")) {
console.log("Voice synthesis cleanly cancelled");
} else {
console.error("Unexpected error:", err);
}
} finally {
// 清理:确保控制器释放(尤其在组件卸载时)
controller.abort();
}
⚠️ 重要注意事项:
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
- 原答案中依赖
setInterval轮询interruptionState的方式存在性能隐患与竞态风险,且未真正终止已启动的 Promise;推荐改用AbortController+Promise.race组合,语义清晰、零依赖、符合 Web 标准。 - 若
send_voice内部调用的是fetch或 WebSocket 发送,务必在其底层也接入signal(如fetch(url, { signal })),否则中断仅作用于外层逻辑。 - 在 React/Vue 等框架中,需在组件
onUnmounted或useEffect cleanup中调用controller.abort(),防止内存泄漏。 - 对于不支持
AbortSignal的旧环境,可用AbortControllerpolyfill,并将interruptionState改为Ref<boolean></boolean>(Vue)或useState(React)配合useRef存储 signal 引用。
总结:Promise 本身不可取消,但异步操作可以设计为可取消。通过将中断逻辑下沉至每个 send_voice 调用内部,并统一由 AbortController 协调,即可实现毫秒级响应中断、零冗余执行的健壮流式语音合成体验。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










