Skip to content

Commit ee64861

Browse files
d-rushmaBaoHuiling
andauthored
RTSP playbackMode and Files Duration Check (#1888)
Co-authored-by: Huiling Bao <huiling.bao@intel.com>
1 parent fc13be8 commit ee64861

27 files changed

+1314
-574
lines changed

education-ai-suite/smart-classroom/api/endpoints.py

Lines changed: 262 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import asyncio
22
from typing import Optional
33
from fastapi import Header, UploadFile
4-
from fastapi.responses import JSONResponse
4+
from fastapi.responses import JSONResponse, FileResponse
55
from fastapi import APIRouter, FastAPI, File, HTTPException, status
66
from dto.transcription_dto import TranscriptionRequest
77
from dto.summarizer_dto import SummaryRequest
88
from dto.video_analytics_dto import VideoAnalyticsRequest
9+
from dto.video_metadata_dto import VideoDurationRequest
910
from pipeline import Pipeline
1011
import json, os
1112
import subprocess, re
@@ -22,6 +23,7 @@
2223
from components.va.va_pipeline_service import VideoAnalyticsPipelineService, PipelineOptions
2324
from utils.session_manager import generate_session_id
2425
from dto.search_dto import SearchRequest
26+
from utils.session_state_manager import SessionState
2527
import logging
2628
logger = logging.getLogger(__name__)
2729

@@ -523,6 +525,126 @@ async def stream_statistics():
523525

524526
return StreamingResponse(stream_statistics(), media_type="application/json")
525527

528+
@router.post("/mark-video-usage")
529+
def mark_video_usage(
530+
session_id: str = Header(None, alias="X-Session-ID")
531+
):
532+
"""
533+
Mark that a video is being used in the current session.
534+
535+
"""
536+
if not session_id:
537+
raise HTTPException(
538+
status_code=400,
539+
detail="X-Session-ID header is required"
540+
)
541+
542+
try:
543+
with SessionState._lock:
544+
if session_id not in SessionState._sessions:
545+
SessionState._sessions[session_id] = {}
546+
SessionState._sessions[session_id]['has_video'] = True
547+
548+
logger.info(f"Session {session_id}: Video usage marked")
549+
return JSONResponse(
550+
status_code=200,
551+
content={"status": "success", "message": "Video usage marked for session"}
552+
)
553+
554+
except Exception as e:
555+
logger.error(f"Session {session_id}: Error marking video usage: {e}")
556+
raise HTTPException(
557+
status_code=500,
558+
detail=f"Error marking video usage: {e}"
559+
)
560+
561+
@router.post("/store-video-duration")
562+
def store_video_duration(
563+
request: VideoDurationRequest,
564+
session_id: str = Header(None, alias="X-Session-ID")
565+
):
566+
"""
567+
Store video duration
568+
569+
"""
570+
if not session_id:
571+
raise HTTPException(
572+
status_code=400,
573+
detail="X-Session-ID header is required"
574+
)
575+
576+
try:
577+
duration = request.duration
578+
579+
if not duration or duration <= 0:
580+
raise HTTPException(
581+
status_code=400,
582+
detail="Invalid duration: duration must be greater than 0"
583+
)
584+
585+
# Store the video duration in session state
586+
SessionState.set_video_duration(session_id, duration)
587+
588+
return JSONResponse(
589+
status_code=200,
590+
content={"status": "success", "message": f"Video duration stored: {duration:.2f}s"}
591+
)
592+
593+
except HTTPException as http_exc:
594+
raise http_exc
595+
except Exception as e:
596+
logger.error(f"Session {session_id}: Error storing video duration: {e}")
597+
raise HTTPException(
598+
status_code=500,
599+
detail=f"Error storing video duration: {e}"
600+
)
601+
602+
@router.post("/store-audio-duration")
603+
def store_audio_duration(
604+
request: VideoDurationRequest,
605+
session_id: str = Header(None, alias="X-Session-ID")
606+
):
607+
"""
608+
Store audio duration
609+
610+
"""
611+
612+
if not session_id:
613+
raise HTTPException(
614+
status_code=400,
615+
detail="X-Session-ID header is required"
616+
)
617+
618+
try:
619+
duration = request.duration
620+
621+
if not duration or duration <= 0:
622+
raise HTTPException(
623+
status_code=400,
624+
detail="Invalid duration: duration must be greater than 0"
625+
)
626+
627+
SessionState.set_audio_duration(session_id, duration)
628+
629+
with SessionState._lock:
630+
if session_id not in SessionState._sessions:
631+
SessionState._sessions[session_id] = {}
632+
SessionState._sessions[session_id]['has_audio'] = True
633+
634+
return JSONResponse(
635+
status_code=200,
636+
content={"status": "success", "message": f"Audio duration stored: {duration:.2f}s"}
637+
)
638+
639+
except HTTPException as http_exc:
640+
raise http_exc
641+
except Exception as e:
642+
logger.error(f"Session {session_id}: Error storing audio duration: {e}")
643+
raise HTTPException(
644+
status_code=500,
645+
detail=f"Error storing audio duration: {e}"
646+
)
647+
526648
@router.post("/content-segmentation")
527649
def content_segmentation(request: SummaryRequest):
528650
"""
@@ -534,17 +656,22 @@ def content_segmentation(request: SummaryRequest):
534656
raise HTTPException(status_code=429, detail="Session Active, Try Later")
535657

536658
pipeline = Pipeline(request.session_id)
659+
660+
# Log session state before validation
661+
session_state = SessionState.get_session_state(request.session_id)
662+
logger.info(f"📋 Content-segmentation request for session: {request.session_id}")
663+
logger.info(f" Session state: {session_state}")
537664

538665
try:
539666
contents_json = pipeline.run_content_segmentation()
540-
logger.info("content segmentation generated successfully.")
541-
JSONResponse(content={"session_id": request.session_id})
667+
logger.info("content segmentation generated successfully.")
668+
return JSONResponse(content={"session_id": request.session_id})
542669

543670
except HTTPException as http_exc:
544671
raise http_exc
545672

546673
except Exception as e:
547-
logger.exception(f"Error during content segmentation: {e}")
674+
logger.exception(f"Error during content segmentation: {e}")
548675
raise HTTPException(
549676
status_code=500,
550677
detail=f"content segmentation failed: {e}"
@@ -576,5 +703,136 @@ def search_content(request: SearchRequest):
576703
detail=f"Search failed: {e}"
577704
)
578705

706+
@router.get("/check-recorded-videos")
707+
def check_recorded_videos(x_session_id: Optional[str] = Header(None)):
708+
"""
709+
Check which video files were saved for a session after RTSP recording.
710+
Returns the priority-ordered available video (back > board > front).
711+
712+
"""
713+
if not x_session_id:
714+
raise HTTPException(
715+
status_code=400, detail="Missing required header: x-session-id"
716+
)
717+
718+
try:
719+
project_config = RuntimeConfig.get_section("Project")
720+
base_path = os.path.join(
721+
project_config.get("location"),
722+
project_config.get("name"),
723+
x_session_id
724+
)
725+
726+
if not os.path.exists(base_path):
727+
logger.warn(f"Session path does not exist: {base_path}")
728+
return JSONResponse(
729+
content={
730+
"session_id": x_session_id,
731+
"back": None,
732+
"board": None,
733+
"front": None,
734+
"selected_video": None,
735+
"message": "No session path found"
736+
},
737+
status_code=200
738+
)
739+
740+
# Check which videos exist
741+
videos = {
742+
"back": None,
743+
"board": None,
744+
"front": None,
745+
}
746+
747+
back_path = os.path.join(base_path, "back.mp4")
748+
content_path = os.path.join(base_path, "content.mp4")
749+
front_path = os.path.join(base_path, "front.mp4")
750+
751+
if os.path.exists(back_path):
752+
videos["back"] = back_path
753+
if os.path.exists(content_path):
754+
videos["board"] = content_path
755+
if os.path.exists(front_path):
756+
videos["front"] = front_path
757+
758+
# Select highest priority video (back > board > front)
759+
selected_video = None
760+
if videos["back"]:
761+
selected_video = "back"
762+
elif videos["board"]:
763+
selected_video = "board"
764+
elif videos["front"]:
765+
selected_video = "front"
766+
767+
return JSONResponse(
768+
content={
769+
"session_id": x_session_id,
770+
"back": videos["back"],
771+
"board": videos["board"],
772+
"front": videos["front"],
773+
"selected_video": selected_video,
774+
"selected_path": videos[selected_video] if selected_video else None
775+
},
776+
status_code=200
777+
)
778+
779+
except Exception as e:
780+
logger.error(f"Error checking recorded videos: {e}")
781+
raise HTTPException(status_code=500, detail=str(e))
782+
783+
@router.get("/recorded-video/{videoType}")
784+
def get_recorded_video(videoType: str, x_session_id: Optional[str] = Header(None), session_id: Optional[str] = None):
785+
"""
786+
Stream a recorded video file (back.mp4, board.mp4, or front.mp4).
787+
788+
"""
789+
# Accept session ID from either header or query parameter
790+
actual_session_id = x_session_id or session_id
791+
if not actual_session_id:
792+
raise HTTPException(
793+
status_code=400, detail="Missing required session ID: x-session-id header or ?session_id query parameter"
794+
)
795+
796+
if videoType not in ['back', 'board', 'front']:
797+
raise HTTPException(
798+
status_code=400, detail=f"Invalid videoType: {videoType}. Must be 'back', 'board', or 'front'"
799+
)
800+
801+
try:
802+
backend_video_type = "content" if videoType == "board" else videoType
803+
804+
project_config = RuntimeConfig.get_section("Project")
805+
video_path = os.path.join(
806+
project_config.get("location"),
807+
project_config.get("name"),
808+
actual_session_id,
809+
f"{backend_video_type}.mp4"
810+
)
811+
812+
if not os.path.exists(video_path):
813+
raise HTTPException(
814+
status_code=404,
815+
detail=f"Video file not found: {videoType}.mp4"
816+
)
817+
818+
logger.info(f"Serving video file: {video_path} for session {actual_session_id}")
819+
820+
file_response = FileResponse(
821+
path=video_path,
822+
media_type="video/mp4",
823+
filename=f"{videoType}.mp4"
824+
)
825+
file_response.headers["Accept-Ranges"] = "bytes"
826+
file_response.headers["Access-Control-Allow-Origin"] = "*"
827+
file_response.headers["Cache-Control"] = "no-cache"
828+
829+
return file_response
830+
831+
except HTTPException:
832+
raise
833+
except Exception as e:
834+
logger.error(f"Error serving recorded video: {e}")
835+
raise HTTPException(status_code=500, detail=str(e))
836+
579837
def register_routes(app: FastAPI):
580838
app.include_router(router)
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
from pydantic import BaseModel
2+
from typing import Optional
3+
4+
class VideoMetadataRequest(BaseModel):
5+
session_id: str
6+
video_file_path: str
7+
8+
class VideoDurationRequest(BaseModel):
9+
duration: float
10+

education-ai-suite/smart-classroom/pipeline.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
from pathlib import Path
1717
import json
1818
from utils.faiss_content_search import FaissContentSearcher
19-
19+
from utils.media_validation_service import MediaValidationService
20+
from utils.session_state_manager import SessionState
2021
import time
2122
logger = logging.getLogger(__name__)
2223

@@ -194,6 +195,19 @@ def run_content_segmentation(self):
194195

195196
transcription_path = os.path.join(session_dir, "transcription.txt")
196197

198+
session_state = SessionState.get_session_state(self.session_id)
199+
# VALIDATION: Check media duration match before processing
200+
is_valid, error_msg = MediaValidationService.validate_duration_match(self.session_id)
201+
202+
if not is_valid:
203+
SessionState.clear_session(self.session_id)
204+
raise HTTPException(
205+
status_code=status.HTTP_400_BAD_REQUEST,
206+
detail=error_msg
207+
)
208+
209+
logger.info(f"✅ Validation passed - proceeding with content segmentation")
210+
197211
try:
198212
transcript_text = StorageManager.read_text_file(transcription_path)
199213

@@ -256,6 +270,9 @@ def run_content_segmentation(self):
256270
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
257271
detail=f"Error during topic segmentation: {e}"
258272
)
273+
finally:
274+
# Clean up session state after processing
275+
SessionState.clear_session(self.session_id)
259276

260277

261278
def search_content(self, query: str, top_k: int = 5):

0 commit comments

Comments
 (0)