@@ -34,7 +34,7 @@ import { remapKeyframeMarkers } from '../utils/tipTapImageUtils';
3434import { persistKeyframeImage } from '../utils/keyframePersistence' ;
3535import { transcribeWithRetry , toAsrLanguage , useAsrSemaphore , isLocalAsrReady , setOnAsrFallback } from '../utils/asrTranscriber' ;
3636import { applySessionReplaces , getSessionHotwordsString , addDynamicBoosts } from '../utils/hotwordRuntime' ;
37- import { cleanAsrResult , dedupeAcrossFinals } from '@/lib/capture/asrFilters' ;
37+ import { cleanAsrResult , dedupeAcrossFinals , estimateAsrConfidence } from '@/lib/capture/asrFilters' ;
3838
3939/** 触发一次增量分析所需的关键帧数 */
4040const INCREMENTAL_BATCH_SIZE = 5 ;
@@ -50,6 +50,8 @@ export interface LiveTranscript {
5050 id : string ;
5151 text : string ;
5252 timestamp : number ;
53+ /** P0-3:转写置信度(估算口径,<0.55 时 UI 弱化标记;缺失视为 1) */
54+ confidence ?: number ;
5355}
5456
5557interface UseClassroomEventsOptions {
@@ -260,10 +262,14 @@ export function useClassroomEvents({
260262 sample_rate : 16000 ,
261263 channels : 1 ,
262264 } , 1 , hotwords || undefined )
263- . then ( ( text ) => {
265+ . then ( ( outcome ) => {
264266 asr . markSuccess ( ) ;
265267 // 输出清洗:相邻重复压缩 + 幻觉过滤(本地路径主进程已 clean,此处兑底云端降级)
266- const cleaned = cleanAsrResult ( text ?? '' ) ;
268+ const cleaned = cleanAsrResult ( outcome ?. text ?? '' ) ;
269+ // P0-3:清洗后文本变化时重估置信度(网关估算基于未清洗文本)
270+ const confidence = cleaned && outcome
271+ ? ( cleaned === outcome . text ? outcome . confidence : estimateAsrConfidence ( outcome . text , cleaned ) )
272+ : 0 ;
267273 // 将转写结果回填到对应的音频段,并剥离 audioBase64 释放内存(单段约 1.2MB,
268274 // 长课堂数百段否则无界累积——内测 5GB 内存主因)。全量分析回退路径优先用
269275 // 已转写的 audioText(sessionAnalyzer: seg.audioText ?? transcribe),无需再持有原始音频;
@@ -281,7 +287,7 @@ export function useClassroomEvents({
281287 // 实时转录上屏(FIFO 上限控制);展示替换后文本(P1-3 替换词后处理),
282288 // 原始清洗后转写经 audioTextRaw 可回溯
283289 setLiveTranscripts ( ( prev ) => {
284- const next = [ ...prev , { id : seg . id , text : applySessionReplaces ( cleaned ) , timestamp : seg . timestampStart } ] ;
290+ const next = [ ...prev , { id : seg . id , text : applySessionReplaces ( cleaned ) , timestamp : seg . timestampStart , confidence } ] ;
285291 return next . length > MAX_LIVE_TRANSCRIPTS
286292 ? next . slice ( next . length - MAX_LIVE_TRANSCRIPTS )
287293 : next ;
@@ -329,7 +335,7 @@ export function useClassroomEvents({
329335 } ) ;
330336 const offFinal = window . electronAPI . on ( 'asr_stream_final' , ( ...args : unknown [ ] ) => {
331337 if ( statusRef . current !== 'capturing' ) return ;
332- const data = args [ 0 ] as { text : string ; timestamp : number } ;
338+ const data = args [ 0 ] as { text : string ; timestamp : number ; confidence ?: number } ;
333339 setPartialText ( '' ) ;
334340 // 双保险:主进程已 clean,此处兜底云端/旧版本主进程的未清洗输出
335341 const cleaned = cleanAsrResult ( data ?. text ?? '' ) ;
@@ -339,11 +345,15 @@ export function useClassroomEvents({
339345 const text = dedupeAcrossFinals ( lastFinalTextRef . current , cleaned ) ;
340346 lastFinalTextRef . current = text || lastFinalTextRef . current ;
341347 if ( ! text ) return ;
348+ // P0-3:置信度透传(主进程估算;本地再清洗后文本变化时重估)
349+ const confidence = cleaned === data . text
350+ ? ( typeof data . confidence === 'number' ? data . confidence : 1 )
351+ : estimateAsrConfidence ( data . text ?? '' , cleaned ) ;
342352 const id = crypto . randomUUID ( ) ;
343353 const timestamp = data . timestamp || Date . now ( ) ;
344354 // 实时转录上屏(FIFO 上限控制);展示替换后文本(P1-3)
345355 setLiveTranscripts ( ( prev ) => {
346- const next = [ ...prev , { id, text : applySessionReplaces ( text ) , timestamp } ] ;
356+ const next = [ ...prev , { id, text : applySessionReplaces ( text ) , timestamp, confidence } ] ;
347357 return next . length > MAX_LIVE_TRANSCRIPTS
348358 ? next . slice ( next . length - MAX_LIVE_TRANSCRIPTS )
349359 : next ;
0 commit comments