@@ -35,6 +35,7 @@ import { persistKeyframeImage } from '../utils/keyframePersistence';
3535import { transcribeWithRetry , toAsrLanguage , useAsrSemaphore , isLocalAsrReady , setOnAsrFallback } from '../utils/asrTranscriber' ;
3636import { applySessionReplaces , getSessionHotwordsString , addDynamicBoosts } from '../utils/hotwordRuntime' ;
3737import { cleanAsrResult , dedupeAcrossFinals , estimateAsrConfidence } from '@/lib/capture/asrFilters' ;
38+ import { classifyContent , hasCommandCue , type ContentKind } from '@/lib/capture/contentClassifier' ;
3839
3940/** 触发一次增量分析所需的关键帧数 */
4041const INCREMENTAL_BATCH_SIZE = 5 ;
@@ -64,11 +65,20 @@ interface UseClassroomEventsOptions {
6465 onNotify : ( type : 'error' , message : string ) => void ;
6566 /** 真流式 ASR 是否激活(激活时跳过按段转写,改用流式 partial/final) */
6667 streamingAsrActive : boolean ;
68+ /** P1-6:目标窗口标题(内容类型分类的标题信号) */
69+ windowTitle ?: string ;
6770}
6871
72+ /** P1-6 分类尝试上限(转写样本仍无信号则停止,unknown 不再重试) */
73+ const MAX_CLASSIFY_ATTEMPTS = 5 ;
74+ /** P1-7 指令补帧节流:最小间隔(ms),防连续指令句触发补帧风暴 */
75+ const FORCE_CAPTURE_MIN_INTERVAL_MS = 1000 ;
76+ /** P1-6 分类转写样本上限(字符,FIFO 追加防无界) */
77+ const MAX_CLASSIFY_SAMPLE_CHARS = 500 ;
78+
6979export function useClassroomEvents ( {
7080 captureManager, status, capturePath, language, aiDetectEnabled, setCourseMeta, onNotify,
71- streamingAsrActive,
81+ streamingAsrActive, windowTitle ,
7282} : UseClassroomEventsOptions ) {
7383 const [ segments , setSegments ] = useState < ExtractedSegment [ ] > ( [ ] ) ;
7484 const [ stats , setStats ] = useState ( { frames : 0 , extracted : 0 } ) ;
@@ -82,11 +92,19 @@ export function useClassroomEvents({
8292 const [ videoFilePath , setVideoFilePath ] = useState < string | null > ( null ) ;
8393 const [ partialCount , setPartialCount ] = useState ( 0 ) ;
8494 const [ transcribedCount , setTranscribedCount ] = useState ( 0 ) ;
95+ /** P1-6 内容类型(课程/软件技能/手法技巧/讲座;驱动采样参数与产物形态) */
96+ const [ contentKind , setContentKind ] = useState < ContentKind > ( 'unknown' ) ;
8597
8698 const partialNotesRef = useRef < string [ ] > ( [ ] ) ;
8799 const pendingKeyframesRef = useRef < KeyFrame [ ] > ( [ ] ) ;
88100 const isPartialAnalyzingRef = useRef ( false ) ;
89101 const courseDetectedRef = useRef ( false ) ;
102+ /** P1-6 分类尝试计数(超上限后停止,unknown 不再重试) */
103+ const classifyAttemptsRef = useRef ( 0 ) ;
104+ /** P1-6 转写样本累积(FIFO 截断) */
105+ const transcriptSampleRef = useRef ( '' ) ;
106+ /** P1-7 指令补帧节流时间戳 */
107+ const lastForceCaptureAtRef = useRef ( 0 ) ;
90108 /** @ai -context 会话时间基准(epoch ms):记录首帧 timestamp,供 analyzePartial 换算相对秒数 */
91109 const sessionStartMsRef = useRef < number | null > ( null ) ;
92110 /** 采集会话 ID(smart:keyframe 事件携带),供笔记持久化关联与关键帧图片清理 */
@@ -110,6 +128,36 @@ export function useClassroomEvents({
110128 } ,
111129 } ) ;
112130
131+ /**
132+ * P1-6/P1-7 转写文本场景消费(ref 桥接,事件回调经此调用避免 effect 重订阅):
133+ * ① 累积分类样本并触发内容分类(最多 5 次尝试,标题信号 > 转写证据)
134+ * ② 指令句检测命中 → 请求强制补帧(1s 节流,防补帧风暴)
135+ */
136+ const sceneProcessorRef = useRef < ( text : string ) => void > ( ( ) => { } ) ;
137+ sceneProcessorRef . current = ( text : string ) => {
138+ if ( ! text ) return ;
139+ // 样本累积(FIFO 截断,防长课无界增长)
140+ transcriptSampleRef . current = ( transcriptSampleRef . current + text ) . slice ( - MAX_CLASSIFY_SAMPLE_CHARS ) ;
141+ // 内容分类:未确定时尝试
142+ if ( contentKind === 'unknown' && classifyAttemptsRef . current < MAX_CLASSIFY_ATTEMPTS ) {
143+ classifyAttemptsRef . current ++ ;
144+ const result = classifyContent ( windowTitle ?? '' , transcriptSampleRef . current ) ;
145+ if ( result . kind !== 'unknown' ) {
146+ setContentKind ( result . kind ) ;
147+ captureManager . applyContentKind ( result . kind ) ;
148+ console . info ( `[useClassroomCapture] 内容类型识别: ${ result . kind } (${ result . source } )` ) ;
149+ }
150+ }
151+ // 指令句补帧(技能场景操作瞬间捕捉)
152+ if ( hasCommandCue ( text ) ) {
153+ const now = Date . now ( ) ;
154+ if ( now - lastForceCaptureAtRef . current >= FORCE_CAPTURE_MIN_INTERVAL_MS ) {
155+ lastForceCaptureAtRef . current = now ;
156+ captureManager . requestForceCapture ( ) ;
157+ }
158+ }
159+ } ;
160+
113161 // ASR 本地→云端降级可见性:注册 toast 回调(会话级节流在 asrTranscriber 内部)
114162 useEffect ( ( ) => {
115163 setOnAsrFallback ( ( ) => toast ( { type : 'warning' , silent : true , message : '本地 ASR 不可用,已降级为云端转写' } ) ) ;
@@ -284,6 +332,8 @@ export function useClassroomEvents({
284332 } ) ) ;
285333 if ( cleaned ) {
286334 setTranscribedCount ( ( c ) => c + 1 ) ;
335+ // P1-6/P1-7:转写文本场景消费(内容分类 + 指令句补帧)
336+ sceneProcessorRef . current ( cleaned ) ;
287337 // 实时转录上屏(FIFO 上限控制);展示替换后文本(P1-3 替换词后处理),
288338 // 原始清洗后转写经 audioTextRaw 可回溯
289339 setLiveTranscripts ( ( prev ) => {
@@ -351,6 +401,8 @@ export function useClassroomEvents({
351401 : estimateAsrConfidence ( data . text ?? '' , cleaned ) ;
352402 const id = crypto . randomUUID ( ) ;
353403 const timestamp = data . timestamp || Date . now ( ) ;
404+ // P1-6/P1-7:转写文本场景消费(内容分类 + 指令句补帧)
405+ sceneProcessorRef . current ( text ) ;
354406 // 实时转录上屏(FIFO 上限控制);展示替换后文本(P1-3)
355407 setLiveTranscripts ( ( prev ) => {
356408 const next = [ ...prev , { id, text : applySessionReplaces ( text ) , timestamp, confidence } ] ;
@@ -483,6 +535,7 @@ export function useClassroomEvents({
483535 vadStats, recordingStatus, setRecordingStatus,
484536 videoFilePath, setVideoFilePath,
485537 partialCount, setPartialCount, transcribedCount,
538+ contentKind,
486539 partialNotesRef, pendingKeyframesRef, isPartialAnalyzingRef,
487540 captureSessionIdRef,
488541 } ;
0 commit comments