播放中打断
All checks were successful
Gitea Actions Demo / Explore-Gitea-Actions (push) Successful in 1m56s

This commit is contained in:
libingxiang 2026-06-23 14:54:04 +08:00
parent 60c8723232
commit 1a9cd283a1

View File

@ -16,6 +16,8 @@ const MIN_VALID_DURATION_SEC = 0.5;
const BROADCAST_TIMEOUT_MS = 60000; const BROADCAST_TIMEOUT_MS = 60000;
const APPID = "i-sfyjfm4jw2q2g"; const APPID = "i-sfyjfm4jw2q2g";
const APP_KEY = "f6mgtnb0abt6ph3rihwm"; const APP_KEY = "f6mgtnb0abt6ph3rihwm";
// 打断词列表:识别到这些词时自动打断播报
const INTERRUPT_KEYWORDS = ["稍等一下", "停一下", "抱歉打断一下", "不好意思", "对不起", "停", "闭嘴"];
/** /**
* *
@ -29,6 +31,17 @@ const hasSubstantialContent = (text: string): boolean => {
return cleaned.length > 0; return cleaned.length > 0;
}; };
/**
*
*/
const containsInterruptKeyword = (text: string): boolean => {
const cleaned = text.replace(
/[\s\u3000.,!?;:,。!?;:、…—\-_·""''()\[\]【】《》<>]/g,
"",
);
return INTERRUPT_KEYWORDS.some((keyword) => cleaned.includes(keyword));
};
enum ReadyState { enum ReadyState {
UNINSTANTIATED = -1, UNINSTANTIATED = -1,
CONNECTING = 0, CONNECTING = 0,
@ -141,6 +154,8 @@ export function useDigitalHuman(options?: {
// 播报超时保护计时器 // 播报超时保护计时器
let broadcastTimeoutId: ReturnType<typeof setTimeout> | null = null; let broadcastTimeoutId: ReturnType<typeof setTimeout> | null = null;
// 打断词检测时间戳用于忽略打断后延迟到达的ASR结果
let lastInterruptTime = 0;
const clearBroadcastTimeout = () => { const clearBroadcastTimeout = () => {
if (broadcastTimeoutId !== null) { if (broadcastTimeoutId !== null) {
@ -164,19 +179,23 @@ export function useDigitalHuman(options?: {
}; };
/** /**
* + *
* RTC模式下保持麦克风开启ASR识别打断词
*/ */
const enterBroadcasting = () => { const enterBroadcasting = () => {
isBroadcasting.value = true; isBroadcasting.value = true;
setPickPhase("broadcasting", "数字人开始播报"); setPickPhase("broadcasting", "数字人开始播报");
startBroadcastTimeout(); startBroadcastTimeout();
// RTC模式播报期间禁用拾音 // RTC模式播报期间保持麦克风开启允许ASR识别打断词
if (audioMode.value === "rtc") { if (audioMode.value === "rtc") {
humanInstanceRef.value?.muteMicrophone?.(true); humanInstanceRef.value?.muteMicrophone?.(false);
isMuted.value = true; isMuted.value = false;
canTransfer.value = false; canTransfer.value = true;
silenceDetector.pause(); const resumed = silenceDetector.resume();
console.info("[播报] RTC模式麦克风已静音静音检测器已暂停"); if (!resumed) {
startSilenceDetection();
}
console.info("[播报] RTC模式麦克风保持开启允许ASR识别打断词");
} }
console.info("[播报] 开始播报等待FINISHED信号"); console.info("[播报] 开始播报等待FINISHED信号");
}; };
@ -306,9 +325,15 @@ export function useDigitalHuman(options?: {
type, type,
} = JSON.parse(content.body); } = JSON.parse(content.body);
if (type === "QUERY") { if (type === "QUERY") {
// 播报期间忽略ASR结果双重守卫麦克风已静音但防止残余回调 // 播报期间检测打断词识别到打断词后仅中断播报并恢复拾音不调用LLM接口
if (pickPhase.value === "broadcasting") { if (pickPhase.value === "broadcasting") {
console.info("[状态机] 播报中收到ASR结果忽略"); const hasInterrupt = containsInterruptKeyword(subtitleContent);
if (hasInterrupt) {
console.info(`[打断词检测] 播报中识别到打断词: "${subtitleContent}"自动打断仅中断播报不调用LLM`);
interrupt();
} else {
console.info(`[打断词检测] 播报中收到ASR结果: "${subtitleContent}",非打断词,忽略`);
}
break; break;
} }
@ -321,6 +346,13 @@ export function useDigitalHuman(options?: {
// idle 状态下收到 QUERY必须验证内容有实质才允许状态转换 // idle 状态下收到 QUERY必须验证内容有实质才允许状态转换
// 环境噪音可能触发 ASR 产生空白/标点内容的 QUERY不应进入识别 // 环境噪音可能触发 ASR 产生空白/标点内容的 QUERY不应进入识别
if (pickPhase.value === "idle") { if (pickPhase.value === "idle") {
// 忽略打断后3秒内到达的延迟ASR结果避免打断词被当作新输入
if (Date.now() - lastInterruptTime < 3000) {
console.info(
"[状态机] 打断后延迟到达的ASR结果忽略",
);
break;
}
const hasSubstance = hasSubstantialContent(subtitleContent); const hasSubstance = hasSubstantialContent(subtitleContent);
if (!hasSubstance) { if (!hasSubstance) {
console.info( console.info(
@ -590,6 +622,8 @@ export function useDigitalHuman(options?: {
console.info("发送打断"); console.info("发送打断");
clearBroadcastTimeout(); clearBroadcastTimeout();
await humanInstanceRef.value?.interrupt?.(); await humanInstanceRef.value?.interrupt?.();
// 记录打断时间戳用于忽略打断后延迟到达的ASR结果
lastInterruptTime = Date.now();
// 打断后播报结束,恢复状态 // 打断后播报结束,恢复状态
if (isBroadcasting.value) { if (isBroadcasting.value) {
isBroadcasting.value = false; isBroadcasting.value = false;