diff --git a/src/app/(modules)/upload/page.tsx b/src/app/(modules)/upload/page.tsx index 7906ba8..9c6ec3a 100644 --- a/src/app/(modules)/upload/page.tsx +++ b/src/app/(modules)/upload/page.tsx @@ -31,6 +31,7 @@ const DETECTION_METHODS = [ // { value: 'yoloe_trained_vl', label: 'YOLOE With Vision Language Model' }, { value: 'culvert_detection', label: 'Culvert Detection' }, { value: 'combined', label: 'Road Defect Detection' }, + { value: 'gemini_video', label: 'Gemini AI Analysis' }, ] as const; export default function UploadPage() { diff --git a/src/components/video-player-section.tsx b/src/components/video-player-section.tsx index d993ee9..26927d2 100644 --- a/src/components/video-player-section.tsx +++ b/src/components/video-player-section.tsx @@ -86,13 +86,70 @@ export default function VideoPlayerSection({ // Logs Reducer const [logs, dispatchLogs] = useReducer(logsReducer, []); + // Memoize a unified data object with frames generated if missing + const normalizedData = useMemo(() => { + if (data.frames && Array.isArray(data.frames) && data.frames.length > 0) { + return data; + } + + // Synthesize frames from lists if not present (like in gemini_video) + const framesMap = new Map(); + + // Sort all detections by their first_detected_frame + const allDetections: any[] = []; + enabledTypes.forEach(type => { + const list = (data as any)[type.listKey]; + if (list && Array.isArray(list)) { + list.forEach((item: any) => { + allDetections.push({ ...item, _detType: type.id }); + }); + } + }); + + allDetections.sort((a, b) => (a.first_detected_frame || 0) - (b.first_detected_frame || 0)); + + // Current counts for sticky stats + const currentCounts: Record = {}; + enabledTypes.forEach(t => { currentCounts[t.frameCountKey] = 0; }); + + allDetections.forEach(det => { + const frameId = det.first_detected_frame || det.frame_number || 0; + + if (!framesMap.has(frameId)) { + framesMap.set(frameId, { frame_id: frameId, detections: [] }); + } + + const frameData = framesMap.get(frameId); + + // Update cumulative counts + const typeConfig = enabledTypes.find(t => t.id === det._detType); + if (typeConfig) { + currentCounts[typeConfig.frameCountKey] = (currentCounts[typeConfig.frameCountKey] || 0) + 1; + } + + const countCopy = { ...currentCounts }; + + frameData.detections.push({ + ...det, + type: det.type || det._detType, + detection_id: det.detection_id || det.id, + count: countCopy + }); + }); + + return { + ...data, + frames: Array.from(framesMap.values()).sort((a, b) => (a.frame_id || 0) - (b.frame_id || 0)) + }; + }, [data, enabledTypes]); + // Custom Hooks - const gpsMap = useGpsMap(data); + const gpsMap = useGpsMap(normalizedData); const { getNearestDetections, sortedDetectionIndices } = useFrameDetectionMap( - data, + normalizedData, detectionType, ); - const { getStickyCounts, sortedFrameIndices } = useCumulativeCounts(data.frames); + const { getStickyCounts, sortedFrameIndices } = useCumulativeCounts(normalizedData.frames || []); // Memoized video URL const videoUrl = useMemo(() => { diff --git a/src/constants/detectionModeConfig.ts b/src/constants/detectionModeConfig.ts index a08cf51..ef78227 100644 --- a/src/constants/detectionModeConfig.ts +++ b/src/constants/detectionModeConfig.ts @@ -136,6 +136,20 @@ export const DETECTION_MODES: Record = { label: 'Culvert Detection', enabledTypes: ['good_culvert', 'defective_culvert'], }, + gemini_video: { + id: 'gemini_video', + label: 'Gemini AI Analysis', + enabledTypes: [ + 'pothole', + 'defected_sign_board', + 'road_crack', + 'damaged_road_marking', + 'good_sign_board', + 'drain_issue', + 'good_culvert', + 'defective_culvert', + ], + }, }; // Fallback config for unknown modes diff --git a/src/types/detection.ts b/src/types/detection.ts index 73c1771..29b9dad 100644 --- a/src/types/detection.ts +++ b/src/types/detection.ts @@ -22,7 +22,8 @@ export type DetectionType = | 'yolo_vl' | 'sam3' | 'yoloe' - | 'yoloe_trained_vl'; + | 'yoloe_trained_vl' + | 'gemini_video'; export interface DetectionListItem { detection_id?: number;