@@ -20,9 +20,11 @@ import type {
2020 KeyFrame ,
2121 RecordingStatus ,
2222 VideoRecording ,
23+ CourseMeta ,
2324} from '@/lib/capture' ;
2425import { analyzeSession , analyzeVideo , analyzePartial , mergeNotes } from '@/lib/ai/sessionAnalyzer' ;
2526import type { AnalyzeResult } from '@/lib/ai/sessionAnalyzer' ;
27+ import { detectCourseFromFrame } from '@/lib/ai/courseDetector' ;
2628import { aiClient } from '@/lib/http/apiClient' ;
2729
2830interface IPCAudioStartResult {
@@ -59,6 +61,14 @@ export function useClassroomCapture() {
5961 const [ analysisResult , setAnalysisResult ] = useState < AnalyzeResult | null > ( null ) ;
6062 const [ analysisError , setAnalysisError ] = useState < string | null > ( null ) ;
6163
64+ // ── 实时转录 ──
65+ const [ liveTranscripts , setLiveTranscripts ] = useState < { id : string ; text : string ; timestamp : number } [ ] > ( [ ] ) ;
66+
67+ // ── 课程上下文 ──
68+ const [ courseMeta , setCourseMeta ] = useState < CourseMeta > ( { } ) ;
69+ const [ aiDetectEnabled , setAiDetectEnabled ] = useState ( false ) ;
70+ const courseDetectedRef = useRef ( false ) ;
71+
6272 // ── Path C 全程录制 ──
6373 const [ recordingStatus , setRecordingStatus ] = useState < RecordingStatus | null > ( null ) ;
6474 const [ videoFilePath , setVideoFilePath ] = useState < string | null > ( null ) ;
@@ -149,6 +159,22 @@ export function useClassroomCapture() {
149159 keyframes : [ ...( prev . keyframes ?? [ ] ) , data . keyframe ] ,
150160 } ) ) ;
151161
162+ // AI 课程识别:仅第 1 帧触发一次
163+ if ( aiDetectEnabled && ! courseDetectedRef . current ) {
164+ courseDetectedRef . current = true ;
165+ detectCourseFromFrame ( data . keyframe . imageBase64 )
166+ . then ( ( detected ) => {
167+ if ( detected ) {
168+ setCourseMeta ( ( prev ) => ( {
169+ ...prev ,
170+ ...detected ,
171+ detectedBy : 'ai' ,
172+ } ) ) ;
173+ }
174+ } )
175+ . catch ( ( ) => { /* 静默降级到规则模式 */ } ) ;
176+ }
177+
152178 // 增量分析:累积到缓冲区,达到批次大小时触发后台分析
153179 pendingKeyframesRef . current . push ( data . keyframe ) ;
154180 if ( pendingKeyframesRef . current . length >= INCREMENTAL_BATCH_SIZE && ! isPartialAnalyzingRef . current ) {
@@ -173,7 +199,7 @@ export function useClassroomCapture() {
173199 ( data ) => setSmartBundle ( data . bundle ) ,
174200 ) ;
175201 return ( ) => { offKeyframe ( ) ; offBundleReady ( ) ; } ;
176- } , [ config . language ] ) ;
202+ } , [ config . language , aiDetectEnabled ] ) ;
177203
178204 // ── Path B:流式 ASR — 语音段完成后立即转写 ──
179205 useEffect ( ( ) => {
@@ -205,7 +231,11 @@ export function useClassroomCapture() {
205231 s . id === seg . id ? { ...s , audioText : text } : s ,
206232 ) ,
207233 } ) ) ;
208- if ( text ) setTranscribedCount ( ( c ) => c + 1 ) ;
234+ if ( text ) {
235+ setTranscribedCount ( ( c ) => c + 1 ) ;
236+ // 实时转录上屏
237+ setLiveTranscripts ( ( prev ) => [ ...prev , { id : seg . id , text, timestamp : seg . timestampStart } ] ) ;
238+ }
209239 } )
210240 . catch ( ( err ) => {
211241 console . warn ( '[useClassroomCapture] 流式 ASR 转写失败:' , err ) ;
@@ -337,6 +367,17 @@ export function useClassroomCapture() {
337367
338368 useEffect ( ( ) => { refreshWindows ( ) ; } , [ refreshWindows ] ) ;
339369
370+ // ── 窗口选中时自动提取课程名(规则模式) ──
371+ const COURSE_KEYWORDS = / ( (?: 高 等 数 学 | 线 性 代 数 | 概 率 论 | 大 学 物 理 | 数 据 结 构 | 操 作 系 统 | 编 译 原 理 | 离 散 数 学 | 复 变 函 数 | 英 语 | 高 数 | 大 物 | C 语 言 | P y t h o n | J a v a | 机 器 学 习 | 深 度 学 习 | 人 工 智 能 | 计 算 机 网 络 | 数 据 库 ) [ ^ \s | ] * ) / ;
372+
373+ useEffect ( ( ) => {
374+ if ( ! selectedWindow ) return ;
375+ const match = selectedWindow . title . match ( COURSE_KEYWORDS ) ;
376+ if ( match && ! courseMeta . courseName ) {
377+ setCourseMeta ( ( prev ) => ( { ...prev , courseName : match [ 1 ] , detectedBy : 'window_title' } ) ) ;
378+ }
379+ } , [ selectedWindow ] ) ; // eslint-disable-line react-hooks/exhaustive-deps
380+
340381 // ── 窗口变化监听(后台轮询 + 最小化容错) ──
341382 const windowMissingCountRef = useRef ( 0 ) ;
342383 const WINDOW_MISSING_THRESHOLD = 10 ; // 连续消失 10 次轮询(~30s)才判定为真正关闭
@@ -396,6 +437,7 @@ export function useClassroomCapture() {
396437 setStats ( { frames : 0 , extracted : 0 } ) ;
397438 setSegments ( [ ] ) ;
398439 setSelectedIds ( new Set ( ) ) ;
440+ setLiveTranscripts ( [ ] ) ;
399441
400442 // 预检网关
401443 try {
@@ -442,6 +484,7 @@ export function useClassroomCapture() {
442484 language : config . language ,
443485 autoInsert : config . autoInsert ,
444486 path : capturePath ,
487+ courseMeta : courseMeta . courseName ? courseMeta : undefined ,
445488 } ) ;
446489 soundPlayer . play ( 'capture_start' ) ;
447490
@@ -639,6 +682,34 @@ export function useClassroomCapture() {
639682 setIsAnalyzing ( false ) ;
640683 } , [ ] ) ;
641684
685+ // ── 笔记→闪卡一键生成 ──
686+ const handleGenerateCards = useCallback ( async ( content : string ) => {
687+ if ( ! window . electronAPI ) return ;
688+ try {
689+ toast ( { type : 'info' , message : '正在从笔记生成闪卡...' } ) ;
690+ await window . electronAPI . invoke ( 'ai_generate_cards' , { content } ) ;
691+ toast ( { type : 'success' , message : '闪卡已生成,可在闪卡模块查看' } ) ;
692+ } catch ( err ) {
693+ console . error ( '[useClassroomCapture] 生成闪卡失败:' , err ) ;
694+ toast ( { type : 'error' , message : '闪卡生成失败,请重试' } ) ;
695+ }
696+ } , [ toast ] ) ;
697+
698+ // ── 课中重点标记 ──
699+ const [ bookmarks , setBookmarks ] = useState < { timestamp : number ; label ?: string } [ ] > ( [ ] ) ;
700+
701+ const handleBookmark = useCallback ( ( ) => {
702+ if ( status !== 'capturing' ) return ;
703+ const now = Date . now ( ) ;
704+ setBookmarks ( ( prev ) => [ ...prev , { timestamp : now } ] ) ;
705+ // 同时插入 smartBundle timeline
706+ setSmartBundle ( ( prev ) => ( {
707+ ...prev ,
708+ timeline : [ ...( prev . timeline ?? [ ] ) , { timestamp : now , type : 'bookmark' as const } ] ,
709+ } ) ) ;
710+ toast ( { type : 'success' , message : `已标记重点 (${ new Date ( now ) . toLocaleTimeString ( ) } )` } ) ;
711+ } , [ status , toast ] ) ;
712+
642713 return {
643714 // 窗口
644715 windows, windowsLoading, selectedWindow, setSelectedWindow, refreshWindows,
@@ -648,12 +719,17 @@ export function useClassroomCapture() {
648719 capturePath, setCapturePath, smartBundle,
649720 // 分析
650721 isAnalyzing, analysisResult, analysisError, partialCount, transcribedCount,
722+ // 实时转录
723+ liveTranscripts,
724+ // 课程上下文
725+ courseMeta, setCourseMeta, aiDetectEnabled, setAiDetectEnabled,
651726 // 录制
652727 recordingStatus, videoFilePath,
653728 // 操作
654729 handleStart, handlePause, handleStop, handleModeChange,
655730 handleToggleSelect, handleConfigChange,
656- handleAnalyze, handleVideoAnalyze, handleDismissAnalysis,
731+ handleAnalyze, handleVideoAnalyze, handleDismissAnalysis, handleGenerateCards,
732+ handleBookmark, bookmarks,
657733 // 派生
658734 canStart : ! ! selectedWindow ,
659735 } ;
0 commit comments