1212
1313import type { BrowserWindow } from 'electron' ;
1414import { logger } from '../../logger.js' ;
15- import { cleanAsrResult } from '../../../src/lib/capture/asrFilters.js' ;
15+ import { cleanAsrResult , computeRms , SILENCE_RMS_THRESHOLD } from '../../../src/lib/capture/asrFilters.js' ;
1616import { getOnlineRecognizer , feedWaveform , type OnlineStream } from './SherpaAsrService.js' ;
1717
1818/** partial 推送节流:两次 partial 推送的最小间隔(ms) */
1919const PARTIAL_EMIT_INTERVAL_MS = 150 ;
20+ /**
21+ * P0-5 CPU 优化:静音块隔块喂入——静音期模型反复 decode 无新内容却持续
22+ * 消耗 CPU,每隔 1 个静音块喂 1 次(端点检测延迟最多增加一个采集块粒度,
23+ * 可接受);非静音块全部喂入保证识别实时性。
24+ */
25+ const SILENT_FEED_SKIP_COUNT = 1 ;
2026
2127// ================================================================
2228// 单例状态
@@ -28,6 +34,16 @@ let _sampleRate = 16000;
2834/** 当前已识别文本(用于变化检测,reset 后清空) */
2935let _lastPartialText = '' ;
3036let _lastPartialEmitAt = 0 ;
37+ /**
38+ * 会话级最新热词(P0-6):渲染进程课程识别成功后经
39+ * local_asr_stream_set_hotwords 更新;每个端点断句后以最新热词重建流
40+ * (createStream(hotwords)),无需重启即可让新词条生效。
41+ */
42+ let _latestHotwords : string | undefined ;
43+ /** 最近一次推送的 final 文本(停止 flush 时与尾句去重) */
44+ let _lastFinalText = '' ;
45+ /** 静音块跳过计数(隔块喂入,P0-5) */
46+ let _silentSkipCounter = 0 ;
3147
3248/** 流式 ASR 是否激活 */
3349export function isStreamingActive ( ) : boolean {
@@ -66,11 +82,14 @@ export function startStreamingAsr(
6682
6783 try {
6884 // 透传热词增强字符串(zipformer-transducer 支持 createStream(hotwords))
85+ _latestHotwords = hotwords ;
6986 _stream = recognizer . createStream ( hotwords ) ;
7087 _win = win ;
7188 _sampleRate = sampleRate ;
7289 _lastPartialText = '' ;
7390 _lastPartialEmitAt = 0 ;
91+ _lastFinalText = '' ;
92+ _silentSkipCounter = 0 ;
7493 logger . info ( `[StreamingASR] 已启动 (sampleRate=${ sampleRate } ${ hotwords ? `, hotwords=${ hotwords } ` : '' } )` ) ;
7594 return { success : true , sampleRate } ;
7695 } catch ( err ) {
@@ -80,6 +99,15 @@ export function startStreamingAsr(
8099 }
81100}
82101
102+ /**
103+ * 更新会话热词(P0-6):渲染进程课程识别成功后调用。
104+ * 不立即重启流(会丢当前句),在下一个端点断句时以最新热词重建流生效。
105+ */
106+ export function updateStreamingHotwords ( hotwords : string | undefined ) : void {
107+ _latestHotwords = hotwords ;
108+ logger . info ( `[StreamingASR] 热词已更新,将在下一断句生效${ hotwords ? ` (${ hotwords . length } chars)` : '' } ` ) ;
109+ }
110+
83111/**
84112 * 喂入一个音频块(Float32 PCM,16k 单声道):解码并推送 partial / final。
85113 *
@@ -100,18 +128,30 @@ export function feedStreamingAsr(audioBuffer: ArrayBuffer, sampleRate?: number):
100128 if ( samples . length === 0 ) return ;
101129 const rate = sampleRate ?? _sampleRate ;
102130
131+ // P0-5 静音隔块喂入:静音块每隔 1 块喂 1 次(端点检测延迟 +1 块粒度),
132+ // 降低静音期(课堂大部分时间)无效 decode 的 CPU 占用
133+ if ( computeRms ( audioBuffer ) < SILENCE_RMS_THRESHOLD ) {
134+ _silentSkipCounter ++ ;
135+ if ( _silentSkipCounter <= SILENT_FEED_SKIP_COUNT ) return ;
136+ _silentSkipCounter = 0 ;
137+ } else {
138+ _silentSkipCounter = 0 ;
139+ }
140+
103141 try {
104142 // 统一适配层:新旧版 sherpa-onnx-node 的 acceptWaveform 签名不同
105143 feedWaveform ( _stream , rate , samples ) ;
106144 while ( recognizer . isReady ( _stream ) ) {
107145 recognizer . decode ( _stream ) ;
108146 }
109147
110- // 端点检测:断句 → 推送 final 并 reset
148+ // 端点检测:断句 → 推送 final 并以最新热词重建流(P0-6 热词生效点)
111149 if ( recognizer . isEndpoint ( _stream ) ) {
112150 // 输出后处理:相邻重复压缩 + 幻觉过滤(静音段重复输出防护)
113151 const finalText = cleanAsrResult ( recognizer . getResult ( _stream ) . text ?? '' ) ;
114- recognizer . reset ( _stream ) ;
152+ _lastFinalText = finalText ;
153+ // 重建流:以会话最新热词 createStream(热词变化无需重启即可生效)
154+ _stream = recognizer . createStream ( _latestHotwords ) ;
115155 _lastPartialText = '' ;
116156 _lastPartialEmitAt = 0 ;
117157 if ( finalText ) {
@@ -153,7 +193,9 @@ export function stopStreamingAsr(): void {
153193 const tailText = recognizer
154194 ? cleanAsrResult ( recognizer . getResult ( _stream ) . text ?? '' )
155195 : '' ;
156- if ( tailText ) {
196+ // P0-4 flush 去重:尾句与最近一次 final 完全一致时不再推送
197+ // (端点已推送过该句,flush 重复上屏是停止瞬间重复的兜底场景)
198+ if ( tailText && tailText !== _lastFinalText ) {
157199 emit ( 'asr_stream_final' , { text : tailText , timestamp : Date . now ( ) } ) ;
158200 }
159201 }
@@ -167,5 +209,7 @@ export function stopStreamingAsr(): void {
167209 _win = null ;
168210 _lastPartialText = '' ;
169211 _lastPartialEmitAt = 0 ;
212+ _lastFinalText = '' ;
213+ _silentSkipCounter = 0 ;
170214 logger . info ( '[StreamingASR] 已停止' ) ;
171215}
0 commit comments