Spaces:
Sleeping
Sleeping
| import { useRef, useState } from "react" | |
| import { ChevronDown, Play, RotateCcw, Upload, Loader2, Clapperboard } from "lucide-react" | |
| import { useDashboard } from "@/lib/dashboard" | |
| import { Button } from "@/components/ui/button" | |
| import { Label } from "@/components/ui/label" | |
| import { Input } from "@/components/ui/input" | |
| import { Textarea } from "@/components/ui/textarea" | |
| import { Slider } from "@/components/ui/slider" | |
| import { Separator } from "@/components/ui/separator" | |
| import { cn } from "@/lib/utils" | |
| const DETECTOR_MODELS = [ | |
| { label: "Nano", value: "yoloe-26n-seg.pt" }, | |
| { label: "Small", value: "yoloe-26s-seg.pt" }, | |
| { label: "Medium", value: "yoloe-26m-seg.pt" }, | |
| { label: "Large", value: "yoloe-26l-seg.pt" }, | |
| { label: "XLarge", value: "yoloe-26x-seg.pt" }, | |
| ] | |
| const IMAGE_SIZES = [320, 640, 960, 1280] | |
| function SliderRow({ | |
| label, | |
| value, | |
| display, | |
| min, | |
| max, | |
| step, | |
| onChange, | |
| }: { | |
| label: string | |
| value: number | |
| display: string | |
| min: number | |
| max: number | |
| step: number | |
| onChange: (v: number) => void | |
| }) { | |
| return ( | |
| <div className="space-y-2"> | |
| <div className="flex items-baseline justify-between"> | |
| <Label>{label}</Label> | |
| <span className="font-mono text-xs text-foreground tabular-nums">{display}</span> | |
| </div> | |
| <Slider | |
| value={[value]} | |
| min={min} | |
| max={max} | |
| step={step} | |
| onValueChange={([v]) => onChange(v)} | |
| /> | |
| </div> | |
| ) | |
| } | |
| export function DetectorPanel() { | |
| const { params, setParam, videoFile, setVideo, run, running } = useDashboard() | |
| const fileRef = useRef<HTMLInputElement>(null) | |
| const [dragging, setDragging] = useState(false) | |
| const [advanced, setAdvanced] = useState(false) | |
| const [loadingDemo, setLoadingDemo] = useState(false) | |
| async function loadDemo() { | |
| setLoadingDemo(true) | |
| try { | |
| const r = await fetch("/demo-video") | |
| const blob = await r.blob() | |
| setVideo(new File([blob], "tiny-trigger-demo.mp4", { type: "video/mp4" })) | |
| } catch { | |
| // silently ignore if demo not available | |
| } finally { | |
| setLoadingDemo(false) | |
| } | |
| } | |
| return ( | |
| <div className="space-y-5"> | |
| {/* dropzone */} | |
| <div | |
| onClick={() => fileRef.current?.click()} | |
| onDragOver={(e) => { | |
| e.preventDefault() | |
| setDragging(true) | |
| }} | |
| onDragLeave={() => setDragging(false)} | |
| onDrop={(e) => { | |
| e.preventDefault() | |
| setDragging(false) | |
| const f = e.dataTransfer.files?.[0] | |
| if (f) setVideo(f) | |
| }} | |
| className={cn( | |
| "group relative flex cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border border-dashed px-4 py-6 text-center transition-colors", | |
| dragging | |
| ? "border-primary/60 bg-primary/5" | |
| : "border-border hover:border-white/20 hover:bg-white/[0.02]", | |
| )} | |
| > | |
| <input | |
| ref={fileRef} | |
| type="file" | |
| accept="video/*" | |
| className="hidden" | |
| onChange={(e) => e.target.files?.[0] && setVideo(e.target.files[0])} | |
| /> | |
| <div className="flex size-9 items-center justify-center rounded-full border border-border bg-white/5 text-muted-foreground group-hover:text-foreground"> | |
| <Upload className="size-4" /> | |
| </div> | |
| {videoFile ? ( | |
| <div className="min-w-0"> | |
| <p className="truncate font-mono text-xs text-foreground">{videoFile.name}</p> | |
| <p className="text-[0.6875rem] text-muted-foreground"> | |
| {(videoFile.size / 1e6).toFixed(1)} MB · click to replace | |
| </p> | |
| </div> | |
| ) : ( | |
| <div> | |
| <p className="text-sm text-foreground">Drop a clip or click to upload</p> | |
| <p className="text-[0.6875rem] text-muted-foreground">mp4 · webm · mov</p> | |
| </div> | |
| )} | |
| </div> | |
| <Button | |
| variant="outline" | |
| size="sm" | |
| className="w-full gap-2 text-muted-foreground" | |
| onClick={loadDemo} | |
| disabled={loadingDemo} | |
| > | |
| {loadingDemo ? <Loader2 className="size-3.5 animate-spin" /> : <Clapperboard className="size-3.5" />} | |
| Use demo video | |
| </Button> | |
| <div className="space-y-2"> | |
| <Label htmlFor="classes">Open-vocabulary classes</Label> | |
| <Textarea | |
| id="classes" | |
| value={params.classes} | |
| onChange={(e) => setParam("classes", e.target.value)} | |
| className="min-h-16 font-mono text-xs leading-relaxed" | |
| placeholder="person, cat, car, chair, cup…" | |
| /> | |
| </div> | |
| <div className="space-y-2"> | |
| <Label htmlFor="model">Detector model</Label> | |
| <select | |
| id="model" | |
| value={params.modelName} | |
| onChange={(e) => setParam("modelName", e.target.value)} | |
| className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" | |
| > | |
| {DETECTOR_MODELS.map((model) => ( | |
| <option key={model.value} value={model.value}> | |
| {model.label} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="space-y-4"> | |
| <SliderRow | |
| label="Confidence" | |
| value={params.confidence} | |
| display={params.confidence.toFixed(2)} | |
| min={0.05} | |
| max={0.9} | |
| step={0.05} | |
| onChange={(v) => setParam("confidence", Number(v.toFixed(2)))} | |
| /> | |
| <SliderRow | |
| label="Sample interval" | |
| value={params.sampleIntervalSec} | |
| display={`${params.sampleIntervalSec.toFixed(2)}s`} | |
| min={0.25} | |
| max={5} | |
| step={0.25} | |
| onChange={(v) => setParam("sampleIntervalSec", Number(v.toFixed(2)))} | |
| /> | |
| <SliderRow | |
| label="Max frames" | |
| value={params.maxFrames} | |
| display={`${params.maxFrames}`} | |
| min={10} | |
| max={300} | |
| step={10} | |
| onChange={(v) => setParam("maxFrames", v)} | |
| /> | |
| </div> | |
| <div> | |
| <button | |
| onClick={() => setAdvanced((v) => !v)} | |
| className="flex w-full items-center justify-between text-xs font-medium text-muted-foreground hover:text-foreground" | |
| > | |
| Advanced | |
| <ChevronDown className={cn("size-3.5 transition-transform", advanced && "rotate-180")} /> | |
| </button> | |
| {advanced && ( | |
| <div className="mt-3 space-y-3 reveal"> | |
| <div className="grid grid-cols-2 gap-3"> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="device">Device</Label> | |
| <Input | |
| id="device" | |
| value={params.device} | |
| onChange={(e) => setParam("device", e.target.value)} | |
| className="font-mono text-xs" | |
| placeholder="auto / cuda:0 / cpu" | |
| /> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="imgsz">Image size</Label> | |
| <select | |
| id="imgsz" | |
| value={params.imageSize} | |
| onChange={(e) => setParam("imageSize", Number(e.target.value))} | |
| className="h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm text-foreground ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2" | |
| > | |
| {IMAGE_SIZES.map((size) => ( | |
| <option key={size} value={size}> | |
| {size} | |
| </option> | |
| ))} | |
| </select> | |
| </div> | |
| </div> | |
| <div className="space-y-1.5"> | |
| <Label htmlFor="maxdet">Max detections</Label> | |
| <Input | |
| id="maxdet" | |
| type="number" | |
| value={params.maxDetections || ""} | |
| onChange={(e) => setParam("maxDetections", Number(e.target.value) || 0)} | |
| className="font-mono text-xs" | |
| placeholder="default" | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| </div> | |
| <Separator /> | |
| <div className="flex gap-2"> | |
| <Button | |
| variant="secondary" | |
| size="default" | |
| className="flex-1" | |
| onClick={() => { | |
| setVideo(null) | |
| }} | |
| disabled={running} | |
| > | |
| <RotateCcw className="size-4" /> | |
| Reset | |
| </Button> | |
| <Button className="flex-[2]" onClick={run} disabled={running}> | |
| {running ? <Loader2 className="size-4 animate-spin" /> : <Play className="size-4" />} | |
| {running ? "Detecting…" : "Run detection"} | |
| </Button> | |
| </div> | |
| </div> | |
| ) | |
| } | |