From 7a47b4d7abfb23b50820101743ef45cee1156b3e Mon Sep 17 00:00:00 2001 From: libingxiang Date: Tue, 23 Jun 2026 14:54:04 +0800 Subject: [PATCH] =?UTF-8?q?=E6=92=AD=E6=94=BE=E4=B8=AD=E6=89=93=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (cherry picked from commit 1a9cd283a1ac24c2a09d2aaf3aa5e095a2c6923a) --- src/hooks/useDigitalHuman.ts | 54 ++++++++++++++++++++++++++++-------- 1 file changed, 43 insertions(+), 11 deletions(-) diff --git a/src/hooks/useDigitalHuman.ts b/src/hooks/useDigitalHuman.ts index 2c7a779..f2a4c0a 100644 --- a/src/hooks/useDigitalHuman.ts +++ b/src/hooks/useDigitalHuman.ts @@ -29,6 +29,17 @@ const hasSubstantialContent = (text: string): boolean => { 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 { UNINSTANTIATED = -1, CONNECTING = 0, @@ -141,6 +152,8 @@ export function useDigitalHuman(options?: { // 播报超时保护计时器 let broadcastTimeoutId: ReturnType | null = null; + // 打断词检测时间戳:用于忽略打断后延迟到达的ASR结果 + let lastInterruptTime = 0; const clearBroadcastTimeout = () => { if (broadcastTimeoutId !== null) { @@ -164,19 +177,23 @@ export function useDigitalHuman(options?: { }; /** - * 进入播报状态:静音麦克风 + 暂停静音检测器(互斥锁) + * 进入播报状态 + * RTC模式下保持麦克风开启,允许ASR识别打断词 */ const enterBroadcasting = () => { isBroadcasting.value = true; setPickPhase("broadcasting", "数字人开始播报"); startBroadcastTimeout(); - // RTC模式:播报期间禁用拾音 + // RTC模式:播报期间保持麦克风开启,允许ASR识别打断词 if (audioMode.value === "rtc") { - humanInstanceRef.value?.muteMicrophone?.(true); - isMuted.value = true; - canTransfer.value = false; - silenceDetector.pause(); - console.info("[播报] RTC模式:麦克风已静音,静音检测器已暂停"); + humanInstanceRef.value?.muteMicrophone?.(false); + isMuted.value = false; + canTransfer.value = true; + const resumed = silenceDetector.resume(); + if (!resumed) { + startSilenceDetection(); + } + console.info("[播报] RTC模式:麦克风保持开启,允许ASR识别打断词"); } console.info("[播报] 开始播报,等待FINISHED信号"); }; @@ -306,9 +323,15 @@ export function useDigitalHuman(options?: { type, } = JSON.parse(content.body); if (type === "QUERY") { - // 播报期间忽略ASR结果(双重守卫:麦克风已静音,但防止残余回调) + // 播报期间检测打断词:识别到打断词后仅中断播报并恢复拾音,不调用LLM接口 if (pickPhase.value === "broadcasting") { - console.info("[状态机] 播报中收到ASR结果,忽略"); + const hasInterrupt = containsInterruptKeyword(subtitleContent); + if (hasInterrupt) { + console.info(`[打断词检测] 播报中识别到打断词: "${subtitleContent}",自动打断(仅中断播报,不调用LLM)`); + interrupt(); + } else { + console.info(`[打断词检测] 播报中收到ASR结果: "${subtitleContent}",非打断词,忽略`); + } break; } @@ -321,6 +344,13 @@ export function useDigitalHuman(options?: { // idle 状态下收到 QUERY:必须验证内容有实质才允许状态转换 // 环境噪音可能触发 ASR 产生空白/标点内容的 QUERY,不应进入识别 if (pickPhase.value === "idle") { + // 忽略打断后3秒内到达的延迟ASR结果(避免打断词被当作新输入) + if (Date.now() - lastInterruptTime < 3000) { + console.info( + "[状态机] 打断后延迟到达的ASR结果,忽略", + ); + break; + } const hasSubstance = hasSubstantialContent(subtitleContent); if (!hasSubstance) { console.info( @@ -496,8 +526,8 @@ export function useDigitalHuman(options?: { token: token.value, wrapperId: "human-wrapper", connectParams: { - ttsPer: "5135", - figureId: "4000288", + ttsPer: "CAP_4189", + figureId: "4000748", resolutionHeight: 1920, resolutionWidth: 1080, inactiveDisconnectSec: 300, @@ -590,6 +620,8 @@ export function useDigitalHuman(options?: { console.info("发送打断"); clearBroadcastTimeout(); await humanInstanceRef.value?.interrupt?.(); + // 记录打断时间戳,用于忽略打断后延迟到达的ASR结果 + lastInterruptTime = Date.now(); // 打断后播报结束,恢复状态 if (isBroadcasting.value) { isBroadcasting.value = false;