refactor(results): support class-based detection summaries
This commit is contained in:
@@ -1,27 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { useParams, useRouter } from 'next/navigation';
|
||||
import { ArrowLeft, TrendingUp } from 'lucide-react';
|
||||
import VideoPlayerSection from '@/components/videoPlayerSection';
|
||||
import { PageHeader } from '@/components/page-header';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CompletedVideoResult, DetectionType } from '@/types';
|
||||
import { getDetectionModeConfig } from '@/constants/detectionModeConfig';
|
||||
import { useVideoResultsQuery } from '../hooks/useVideoResults';
|
||||
import { VideoResultsSkeleton } from './components/VideoResultsSkeleton';
|
||||
|
||||
const inferDetectionType = (data: CompletedVideoResult): DetectionType => {
|
||||
if (data.detection_mode) return data.detection_mode as DetectionType;
|
||||
|
||||
const signboards = data.summary?.unique_signboards || 0;
|
||||
const potholes = data.summary?.unique_potholes || 0;
|
||||
|
||||
if (signboards > 0 && potholes > 0) return 'pot-sign-detection';
|
||||
if (signboards > 0) return 'sign-board-detection';
|
||||
return 'pothole-detection';
|
||||
};
|
||||
|
||||
export default function VideoResultsPage() {
|
||||
const router = useRouter();
|
||||
const { videoId } = useParams() as { videoId: string };
|
||||
@@ -33,30 +19,19 @@ export default function VideoResultsPage() {
|
||||
error: queryError,
|
||||
} = useVideoResultsQuery(videoId);
|
||||
|
||||
const detectionType = useMemo<DetectionType>(
|
||||
() =>
|
||||
detectionData ? inferDetectionType(detectionData) : 'pothole-detection',
|
||||
[detectionData],
|
||||
);
|
||||
|
||||
const error = isError
|
||||
? queryError instanceof Error
|
||||
? queryError.message
|
||||
: 'Failed to load results'
|
||||
: null;
|
||||
|
||||
const getTitle = () => {
|
||||
const config = getDetectionModeConfig(detectionType);
|
||||
return `${config.label} Results`;
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <VideoResultsSkeleton />;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-[60vh] flex items-center justify-center">
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<p className="text-sm text-destructive">{error}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -65,7 +40,7 @@ export default function VideoResultsPage() {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<PageHeader
|
||||
title={'Detection Results'}
|
||||
title="Detection Results"
|
||||
description={`Video ID: ${videoId}`}
|
||||
icon={TrendingUp}
|
||||
actions={
|
||||
|
||||
@@ -1,48 +1,36 @@
|
||||
'use client';
|
||||
|
||||
import { Activity, AlertTriangle, MapPin, SignpostBig } from 'lucide-react';
|
||||
import { Activity, MapPin } from 'lucide-react';
|
||||
|
||||
import type { CompletedVideoResult, DetectionResultLog } from '@/types';
|
||||
|
||||
import { getDefectVisual } from './defectVisualConfig';
|
||||
|
||||
interface CurrentDetectionBarProps {
|
||||
activeLog?: DetectionResultLog;
|
||||
summary: CompletedVideoResult['summary'];
|
||||
data: CompletedVideoResult;
|
||||
}
|
||||
|
||||
export default function CurrentDetectionBar({
|
||||
activeLog,
|
||||
summary,
|
||||
data,
|
||||
}: CurrentDetectionBarProps) {
|
||||
const currentCounts = activeLog?.cumulative_counts || summary;
|
||||
const countItems = [
|
||||
{
|
||||
label: 'Potholes',
|
||||
value: currentCounts.unique_potholes || 0,
|
||||
icon: AlertTriangle,
|
||||
className: 'text-orange-500',
|
||||
iconClassName: 'bg-orange-500/10 text-orange-500',
|
||||
},
|
||||
{
|
||||
label: 'Signboards',
|
||||
value: currentCounts.unique_signboards || 0,
|
||||
icon: SignpostBig,
|
||||
className: 'text-blue-500',
|
||||
iconClassName: 'bg-blue-500/10 text-blue-500',
|
||||
},
|
||||
{
|
||||
label: 'Total',
|
||||
value: currentCounts.total_detections || 0,
|
||||
icon: Activity,
|
||||
className: 'text-green-500',
|
||||
iconClassName: 'bg-green-500/10 text-green-500',
|
||||
},
|
||||
];
|
||||
const latitude = activeLog?.latitude;
|
||||
const longitude = activeLog?.longitude;
|
||||
const defectCounts = activeLog
|
||||
? activeLog.running_counts.by_class
|
||||
: data.defect_counts;
|
||||
const totalDetections = activeLog
|
||||
? activeLog.running_counts.total
|
||||
: data.summary.total_detections;
|
||||
const { latitude, longitude } = activeLog?.location ?? {
|
||||
latitude: undefined,
|
||||
longitude: undefined,
|
||||
};
|
||||
const coordinates =
|
||||
typeof latitude === 'number' && typeof longitude === 'number'
|
||||
? `${latitude.toFixed(7)}, ${longitude.toFixed(7)}`
|
||||
: undefined;
|
||||
const gridColumns =
|
||||
defectCounts.length >= 3 ? 'sm:grid-cols-3' : 'sm:grid-cols-2';
|
||||
|
||||
return (
|
||||
<section className="overflow-hidden rounded-lg border bg-card/80">
|
||||
@@ -67,33 +55,48 @@ export default function CurrentDetectionBar({
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-2 p-3 sm:grid-cols-3">
|
||||
{countItems.map((item) => {
|
||||
const Icon = item.icon;
|
||||
<div className={`grid grid-cols-1 gap-2 p-3 ${gridColumns}`}>
|
||||
{defectCounts.map((item) => {
|
||||
const visual = getDefectVisual(item.class_name);
|
||||
const Icon = visual.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.label}
|
||||
key={item.class_name}
|
||||
className="flex items-center justify-between gap-3 rounded-md bg-muted/25 px-3 py-2.5 ring-1 ring-border/60"
|
||||
>
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span
|
||||
className={`flex size-8 shrink-0 items-center justify-center rounded-md ${item.iconClassName}`}
|
||||
className={`flex size-8 shrink-0 items-center justify-center rounded-md ${visual.iconClassName}`}
|
||||
>
|
||||
<Icon className="size-4" />
|
||||
</span>
|
||||
<p className="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
{item.label}
|
||||
{item.display_name}
|
||||
</p>
|
||||
</div>
|
||||
<p
|
||||
className={`text-2xl font-bold leading-none ${item.className}`}
|
||||
className={`text-2xl font-bold leading-none ${visual.colorClassName}`}
|
||||
>
|
||||
{item.value}
|
||||
{item.count}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
<div className="flex items-center justify-between gap-3 rounded-md bg-muted/25 px-3 py-2.5 ring-1 ring-border/60">
|
||||
<div className="flex min-w-0 items-center gap-3">
|
||||
<span className="flex size-8 shrink-0 items-center justify-center rounded-md bg-green-500/10 text-green-500">
|
||||
<Activity className="size-4" />
|
||||
</span>
|
||||
<p className="truncate text-xs font-semibold uppercase tracking-wide text-muted-foreground">
|
||||
Total
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-2xl font-bold leading-none text-green-500">
|
||||
{totalDetections}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
54
src/components/video/defectVisualConfig.ts
Normal file
54
src/components/video/defectVisualConfig.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
CircleAlert,
|
||||
CircleDot,
|
||||
Droplets,
|
||||
LucideIcon,
|
||||
SignpostBig,
|
||||
} from 'lucide-react';
|
||||
|
||||
type DefectVisualConfig = {
|
||||
icon: LucideIcon;
|
||||
colorClassName: string;
|
||||
iconClassName: string;
|
||||
};
|
||||
|
||||
const DEFECT_VISUALS: Record<string, DefectVisualConfig> = {
|
||||
manhole_cover: {
|
||||
icon: CircleDot,
|
||||
colorClassName: 'text-zinc-500',
|
||||
iconClassName: 'bg-zinc-500/10 text-zinc-500',
|
||||
},
|
||||
pothole: {
|
||||
icon: AlertTriangle,
|
||||
colorClassName: 'text-orange-500',
|
||||
iconClassName: 'bg-orange-500/10 text-orange-500',
|
||||
},
|
||||
road_crack: {
|
||||
icon: CircleAlert,
|
||||
colorClassName: 'text-rose-500',
|
||||
iconClassName: 'bg-rose-500/10 text-rose-500',
|
||||
},
|
||||
sign_board: {
|
||||
icon: SignpostBig,
|
||||
colorClassName: 'text-blue-500',
|
||||
iconClassName: 'bg-blue-500/10 text-blue-500',
|
||||
},
|
||||
water_puddle: {
|
||||
icon: Droplets,
|
||||
colorClassName: 'text-cyan-500',
|
||||
iconClassName: 'bg-cyan-500/10 text-cyan-500',
|
||||
},
|
||||
};
|
||||
|
||||
const DEFAULT_VISUAL: DefectVisualConfig = {
|
||||
icon: Activity,
|
||||
colorClassName: 'text-green-500',
|
||||
iconClassName: 'bg-green-500/10 text-green-500',
|
||||
};
|
||||
|
||||
export const getDefectVisual = (className: string) =>
|
||||
DEFECT_VISUALS[className] || DEFAULT_VISUAL;
|
||||
@@ -2,10 +2,8 @@
|
||||
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { Activity, Film, MapPin } from 'lucide-react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { DetectionResultLog } from '@/types';
|
||||
import { DETECTION_TYPES } from '@/constants/detectionModeConfig';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface DetectionLogsProps {
|
||||
@@ -34,7 +32,7 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h4 className="text-sm font-bold flex items-center gap-2">
|
||||
<h4 className="flex items-center gap-2 text-sm font-bold">
|
||||
<Activity className="h-4 w-4" />
|
||||
Detection Logs
|
||||
</h4>
|
||||
@@ -43,20 +41,18 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
||||
<div className="space-y-3 p-4">
|
||||
{logs.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-muted-foreground">
|
||||
<Film className="h-8 w-8 mb-2 opacity-20" />
|
||||
<Film className="mb-2 h-8 w-8 opacity-20" />
|
||||
<p className="text-xs">No detections found</p>
|
||||
</div>
|
||||
) : (
|
||||
logs.map((log) => {
|
||||
const typeId = (log.type || '').toLowerCase();
|
||||
const typeConfig = DETECTION_TYPES[typeId];
|
||||
const label =
|
||||
log.label || typeConfig?.label || typeId.replace(/_/g, ' ');
|
||||
const timestampSeconds = log.frame.timestamp_seconds;
|
||||
const { latitude, longitude } = log.location;
|
||||
const isActive = activeLogId === log.id;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={`${log.id}-${log.frame}`}
|
||||
key={`${log.id}-${log.frame.number}`}
|
||||
ref={isActive ? activeLogRef : null}
|
||||
type="button"
|
||||
onClick={() => onSeek(log)}
|
||||
@@ -65,35 +61,35 @@ const DetectionLogs = ({ logs, activeLogId, onSeek }: DetectionLogsProps) => {
|
||||
isActive && 'border-primary bg-primary/5',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4 mb-3">
|
||||
<div className="mb-3 flex items-start justify-between gap-4">
|
||||
<div>
|
||||
<div className="text-[13px] font-bold">
|
||||
{label} ID: {log.id}
|
||||
{log.detection.display_name} ID: {log.id}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground font-medium">
|
||||
Frame {log.frame}
|
||||
<div className="text-[11px] font-medium text-muted-foreground">
|
||||
Frame {log.frame.number}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right shrink-0">
|
||||
<div className="shrink-0 text-right">
|
||||
<div className="text-xs font-bold">
|
||||
{formatVideoTime(log.timestamp_seconds)}
|
||||
{formatVideoTime(timestampSeconds)}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground">
|
||||
{log.timestamp_seconds.toFixed(2)}s
|
||||
{timestampSeconds.toFixed(2)}s
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 text-[12px] text-muted-foreground/90 font-medium">
|
||||
{typeof log.confidence === 'number' && (
|
||||
<div>Confidence: {(log.confidence * 100).toFixed(1)}%</div>
|
||||
)}
|
||||
{typeof log.latitude === 'number' &&
|
||||
typeof log.longitude === 'number' && (
|
||||
<div className="space-y-2 text-[12px] font-medium text-muted-foreground/90">
|
||||
<div>
|
||||
Confidence: {(log.detection.confidence * 100).toFixed(1)}%
|
||||
</div>
|
||||
{typeof latitude === 'number' &&
|
||||
typeof longitude === 'number' && (
|
||||
<div className="flex items-center gap-1.5">
|
||||
<MapPin className="h-3 w-3" />
|
||||
<span>
|
||||
{log.latitude.toFixed(8)}, {log.longitude.toFixed(8)}
|
||||
{latitude.toFixed(8)}, {longitude.toFixed(8)}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -1,81 +1,80 @@
|
||||
'use client';
|
||||
|
||||
import {
|
||||
Activity,
|
||||
AlertTriangle,
|
||||
Clock,
|
||||
Gauge,
|
||||
SignpostBig,
|
||||
} from 'lucide-react';
|
||||
import { Activity, Clock, Gauge } from 'lucide-react';
|
||||
import { CompletedVideoResult } from '@/types';
|
||||
|
||||
const formatDuration = (seconds: number) => {
|
||||
const safeSeconds = Number.isFinite(seconds) ? seconds : 0;
|
||||
const minutes = Math.floor(safeSeconds / 60);
|
||||
const secs = Math.floor(safeSeconds % 60);
|
||||
import { getDefectVisual } from './defectVisualConfig';
|
||||
|
||||
const formatDuration = (seconds?: number) => {
|
||||
if (typeof seconds !== 'number' || !Number.isFinite(seconds)) return '-';
|
||||
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${minutes}:${secs.toString().padStart(2, '0')}`;
|
||||
};
|
||||
|
||||
const formatFps = (fps?: number) =>
|
||||
typeof fps === 'number' && Number.isFinite(fps) ? fps.toFixed(1) : '-';
|
||||
|
||||
interface ResultStatsGridProps {
|
||||
data: CompletedVideoResult;
|
||||
}
|
||||
|
||||
export default function ResultStatsGrid({ data }: ResultStatsGridProps) {
|
||||
const defectStats = data.defect_counts.map((item) => {
|
||||
const visual = getDefectVisual(item.class_name);
|
||||
|
||||
return {
|
||||
label: item.display_name,
|
||||
value: item.count,
|
||||
icon: visual.icon,
|
||||
color: visual.colorClassName,
|
||||
bgColor: visual.iconClassName,
|
||||
};
|
||||
});
|
||||
|
||||
const stats = [
|
||||
{
|
||||
label: 'Total Detections',
|
||||
value: data.summary.total_detections || 0,
|
||||
value: data.summary.total_detections,
|
||||
icon: Activity,
|
||||
color: 'text-green-500',
|
||||
bgColor: 'bg-green-500/10',
|
||||
},
|
||||
{
|
||||
label: 'Potholes',
|
||||
value: data.summary.unique_potholes || 0,
|
||||
icon: AlertTriangle,
|
||||
color: 'text-orange-500',
|
||||
bgColor: 'bg-orange-500/10',
|
||||
},
|
||||
{
|
||||
label: 'Signboards',
|
||||
value: data.summary.unique_signboards || 0,
|
||||
icon: SignpostBig,
|
||||
color: 'text-blue-500',
|
||||
bgColor: 'bg-blue-500/10',
|
||||
bgColor: 'bg-green-500/10 text-green-500',
|
||||
},
|
||||
...defectStats,
|
||||
{
|
||||
label: 'FPS',
|
||||
value: data.fps.toFixed(1),
|
||||
value: formatFps(data.summary.fps),
|
||||
icon: Gauge,
|
||||
color: 'text-purple-500',
|
||||
bgColor: 'bg-purple-500/10',
|
||||
bgColor: 'bg-purple-500/10 text-purple-500',
|
||||
},
|
||||
{
|
||||
label: 'Duration',
|
||||
value: formatDuration(data.duration_seconds),
|
||||
value: formatDuration(data.summary.duration_seconds),
|
||||
icon: Clock,
|
||||
color: 'text-cyan-500',
|
||||
bgColor: 'bg-cyan-500/10',
|
||||
bgColor: 'bg-cyan-500/10 text-cyan-500',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-3">
|
||||
<div className="grid grid-cols-2 gap-3 md:grid-cols-3 xl:grid-cols-5">
|
||||
{stats.map((stat) => {
|
||||
const Icon = stat.icon;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={stat.label}
|
||||
className="p-4 rounded-lg bg-muted/30 border border-muted flex flex-col items-center text-center"
|
||||
className="flex flex-col items-center rounded-lg border border-muted bg-muted/30 p-4 text-center"
|
||||
>
|
||||
<div className={`p-2 rounded-md mb-2 ${stat.bgColor}`}>
|
||||
<Icon className={`h-5 w-5 ${stat.color}`} />
|
||||
<div className={`mb-2 rounded-md p-2 ${stat.bgColor}`}>
|
||||
<Icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className={`text-xl font-bold ${stat.color}`}>
|
||||
{stat.value}
|
||||
</div>
|
||||
<div className="text-[10px] font-bold uppercase tracking-wider text-muted-foreground mt-1">
|
||||
<div className="mt-1 text-[10px] font-bold uppercase tracking-wider text-muted-foreground">
|
||||
{stat.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -14,7 +14,10 @@ export function useDetectionPlayback({
|
||||
}: UseDetectionPlaybackParams) {
|
||||
const shouldPauseAfterSeekRef = useRef(false);
|
||||
const sortedLogs = useMemo(
|
||||
() => [...(logs || [])].sort((a, b) => a.timestamp_seconds - b.timestamp_seconds),
|
||||
() =>
|
||||
[...(logs || [])].sort(
|
||||
(a, b) => a.frame.timestamp_seconds - b.frame.timestamp_seconds,
|
||||
),
|
||||
[logs],
|
||||
);
|
||||
const [activeLog, setActiveLog] = useState<DetectionResultLog | undefined>(
|
||||
@@ -27,7 +30,7 @@ export function useDetectionPlayback({
|
||||
|
||||
shouldPauseAfterSeekRef.current = true;
|
||||
video.pause();
|
||||
video.currentTime = log.timestamp_seconds;
|
||||
video.currentTime = log.frame.timestamp_seconds;
|
||||
setActiveLog(log);
|
||||
};
|
||||
|
||||
@@ -44,7 +47,7 @@ export function useDetectionPlayback({
|
||||
if (!video || sortedLogs.length === 0) return;
|
||||
|
||||
const currentLog = sortedLogs.findLast(
|
||||
(log) => log.timestamp_seconds <= video.currentTime,
|
||||
(log) => log.frame.timestamp_seconds <= video.currentTime,
|
||||
);
|
||||
|
||||
if (currentLog && currentLog.id !== activeLog?.id) {
|
||||
|
||||
@@ -21,9 +21,7 @@ type VideoPlayerSectionProps = {
|
||||
data: CompletedVideoResult;
|
||||
};
|
||||
|
||||
export default function VideoPlayerSection({
|
||||
data,
|
||||
}: VideoPlayerSectionProps) {
|
||||
export default function VideoPlayerSection({ data }: VideoPlayerSectionProps) {
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const {
|
||||
activeLog,
|
||||
@@ -45,9 +43,9 @@ export default function VideoPlayerSection({
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<Card className="overflow-hidden">
|
||||
<CardHeader className="pb-4 border-b">
|
||||
<CardHeader className="border-b pb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="p-2 rounded bg-secondary">
|
||||
<div className="rounded bg-secondary p-2">
|
||||
<Film className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
@@ -61,8 +59,8 @@ export default function VideoPlayerSection({
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="pt-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<AnnotatedVideoPlayer
|
||||
videoRef={videoRef}
|
||||
videoUrl={videoUrl}
|
||||
@@ -73,10 +71,7 @@ export default function VideoPlayerSection({
|
||||
onSeeked={handleSeeked}
|
||||
/>
|
||||
|
||||
<CurrentDetectionBar
|
||||
activeLog={activeLog}
|
||||
summary={data.summary}
|
||||
/>
|
||||
<CurrentDetectionBar activeLog={activeLog} data={data} />
|
||||
</div>
|
||||
|
||||
<DetectionLogs
|
||||
@@ -91,7 +86,6 @@ export default function VideoPlayerSection({
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
/**
|
||||
* Detection mode configurations to support dynamic UI behavior
|
||||
*/
|
||||
|
||||
export interface DetectionTypeConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface DetectionModeConfig {
|
||||
id: string;
|
||||
label: string;
|
||||
enabledTypes: string[]; // IDs of detection types to show in this mode
|
||||
}
|
||||
|
||||
export const DETECTION_TYPES: Record<string, DetectionTypeConfig> = {
|
||||
pothole: {
|
||||
id: 'pothole',
|
||||
label: 'Pothole',
|
||||
color: 'var(--chart-1)',
|
||||
},
|
||||
defected_sign_board: {
|
||||
id: 'defected_sign_board',
|
||||
label: 'Defect Sign Board',
|
||||
color: 'var(--chart-2)',
|
||||
},
|
||||
road_crack: {
|
||||
id: 'road_crack',
|
||||
label: 'Road Crack',
|
||||
color: 'var(--chart-3)',
|
||||
},
|
||||
damaged_road_marking: {
|
||||
id: 'damaged_road_marking',
|
||||
label: 'Damage Road Mark',
|
||||
color: 'var(--chart-4)',
|
||||
},
|
||||
good_sign_board: {
|
||||
id: 'good_sign_board',
|
||||
label: 'Good Sign Board',
|
||||
color: 'var(--chart-5)',
|
||||
},
|
||||
drain_issue: {
|
||||
id: 'drain_issue',
|
||||
label: 'Drain Issue',
|
||||
color: 'var(--chart-6)',
|
||||
},
|
||||
good_culvert: {
|
||||
id: 'good_culvert',
|
||||
label: 'Good Culvert',
|
||||
color: 'var(--chart-5)',
|
||||
},
|
||||
defective_culvert: {
|
||||
id: 'defective_culvert',
|
||||
label: 'Defective Culvert',
|
||||
color: 'var(--chart-8)',
|
||||
},
|
||||
};
|
||||
|
||||
export const DETECTION_MODES: Record<string, DetectionModeConfig> = {
|
||||
yolo: {
|
||||
id: 'yolo',
|
||||
label: 'YOLO Detection',
|
||||
enabledTypes: [
|
||||
'pothole',
|
||||
'defected_sign_board',
|
||||
'road_crack',
|
||||
'damaged_road_marking',
|
||||
'good_sign_board',
|
||||
'drain_issue',
|
||||
],
|
||||
},
|
||||
yolo_vl: {
|
||||
id: 'yolo_vl',
|
||||
label: 'YOLO-VL Detection',
|
||||
enabledTypes: [
|
||||
'pothole',
|
||||
'defected_sign_board',
|
||||
'road_crack',
|
||||
'damaged_road_marking',
|
||||
'good_sign_board',
|
||||
'drain_issue',
|
||||
],
|
||||
},
|
||||
'pothole-detection': {
|
||||
id: 'pothole-detection',
|
||||
label: 'Pothole Detection',
|
||||
enabledTypes: [
|
||||
'pothole',
|
||||
'road_crack',
|
||||
'damaged_road_marking',
|
||||
'drain_issue',
|
||||
],
|
||||
},
|
||||
'sign-board-detection': {
|
||||
id: 'sign-board-detection',
|
||||
label: 'Signboard Detection',
|
||||
enabledTypes: ['defected_sign_board', 'good_sign_board'],
|
||||
},
|
||||
'pot-sign-detection': {
|
||||
id: 'pot-sign-detection',
|
||||
label: 'Pothole & Signboard Detection',
|
||||
enabledTypes: [
|
||||
'pothole',
|
||||
'defected_sign_board',
|
||||
'road_crack',
|
||||
'damaged_road_marking',
|
||||
'good_sign_board',
|
||||
'drain_issue',
|
||||
],
|
||||
},
|
||||
culvert_detection: {
|
||||
id: 'culvert_detection',
|
||||
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
|
||||
export const DEFAULT_DETECTION_MODE: DetectionModeConfig =
|
||||
DETECTION_MODES['pot-sign-detection'];
|
||||
|
||||
export const getDetectionModeConfig = (mode?: string): DetectionModeConfig => {
|
||||
if (!mode) return DEFAULT_DETECTION_MODE;
|
||||
return DETECTION_MODES[mode] || DEFAULT_DETECTION_MODE;
|
||||
};
|
||||
|
||||
export const getEnabledDetectionTypes = (
|
||||
mode?: string,
|
||||
): DetectionTypeConfig[] => {
|
||||
const config = getDetectionModeConfig(mode);
|
||||
return config.enabledTypes
|
||||
.map((typeId) => DETECTION_TYPES[typeId])
|
||||
.filter(Boolean);
|
||||
};
|
||||
@@ -1,19 +1,15 @@
|
||||
import { DetectionResultLog } from './detection';
|
||||
import { DetectionClassCount, DetectionResultLog } from './detection';
|
||||
|
||||
export type CompletedVideoResult = {
|
||||
video_id: string;
|
||||
status: 'completed' | string;
|
||||
// annotated_video_url: string;
|
||||
processed_video_url: string | null;
|
||||
raw_video_url: string | null;
|
||||
fps: number;
|
||||
duration_seconds: number;
|
||||
detection_mode?: string;
|
||||
summary: {
|
||||
fps?: number;
|
||||
duration_seconds?: number;
|
||||
total_detections: number;
|
||||
unique_potholes?: number;
|
||||
unique_signboards?: number;
|
||||
[key: string]: number | undefined;
|
||||
};
|
||||
defect_counts: DetectionClassCount[];
|
||||
logs: DetectionResultLog[];
|
||||
};
|
||||
|
||||
@@ -1,43 +1,32 @@
|
||||
/**
|
||||
* Detection types for map and display
|
||||
*/
|
||||
export interface Detection {
|
||||
id: number;
|
||||
video_id: string;
|
||||
type: string;
|
||||
class: string;
|
||||
confidence: number;
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
frame_number: number;
|
||||
timestamp_ms: number;
|
||||
}
|
||||
|
||||
export type DetectionType =
|
||||
| 'pothole-detection'
|
||||
| 'sign-board-detection'
|
||||
| 'pot-sign-detection'
|
||||
| 'culvert_detection'
|
||||
| 'yolo'
|
||||
| 'yolo_vl'
|
||||
| 'sam3'
|
||||
| 'yoloe'
|
||||
| 'yoloe_trained_vl'
|
||||
| 'gemini_video';
|
||||
export type DetectionClassCount = {
|
||||
class_name: string;
|
||||
display_name: string;
|
||||
count: number;
|
||||
unique_count: number;
|
||||
};
|
||||
|
||||
export type DetectionResultLog = {
|
||||
id: string;
|
||||
type: string;
|
||||
label?: string;
|
||||
frame: number;
|
||||
detection: {
|
||||
id: number;
|
||||
class_name: string;
|
||||
display_name: string;
|
||||
confidence: number;
|
||||
};
|
||||
frame: {
|
||||
number: number;
|
||||
timestamp_seconds: number;
|
||||
confidence?: number;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
cumulative_counts?: {
|
||||
unique_potholes?: number;
|
||||
unique_signboards?: number;
|
||||
total_detections?: number;
|
||||
[key: string]: number | undefined;
|
||||
};
|
||||
location: {
|
||||
latitude: number | null;
|
||||
longitude: number | null;
|
||||
};
|
||||
running_counts: {
|
||||
total: number;
|
||||
by_class: Array<{
|
||||
class_name: string;
|
||||
display_name: string;
|
||||
count: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user