salvirezwan's picture
Add HF Spaces deployment
17c8768
Raw
History Blame Contribute Delete
8.56 kB
/**
* PipelineControl — start / stop pipeline form.
*
* Features:
* - Free-form source input (webcam index, file path, RTSP URL)
* - "Demo" quick-start button — synthetic scene, no model needed
* - Optional counting line configuration (up to 3 named lines)
* - Confidence threshold
*/
import { useState } from 'react'
const API = '/api'
// Default counting line for demo mode: vertical centre line on a 1280×720 frame
const DEMO_LINE = { name: 'main', x1: 640, y1: 252, x2: 640, y2: 612 }
export default function PipelineControl({ status, onStatusChange, gpu }) {
const [source, setSource] = useState('0')
const [conf, setConf] = useState('0.35')
const [showLines, setShowLines] = useState(false)
const [lines, setLines] = useState([])
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null)
const isRunning = status?.running ?? false
// ── helpers ──────────────────────────────────────────────────────────────
function addLine() {
if (lines.length >= 3) return
setLines(prev => [
...prev,
{ name: `line${prev.length + 1}`, x1: 0, y1: 360, x2: 1280, y2: 360 },
])
}
function removeLine(i) {
setLines(prev => prev.filter((_, idx) => idx !== i))
}
function updateLine(i, field, value) {
setLines(prev =>
prev.map((l, idx) =>
idx === i ? { ...l, [field]: field === 'name' ? value : Number(value) } : l
)
)
}
async function startPipeline(src, linesCfg) {
setLoading(true)
setError(null)
try {
const res = await fetch(`${API}/pipeline/start`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
source: src,
confidence_threshold: parseFloat(conf),
counting_lines: linesCfg,
}),
})
if (!res.ok) {
const body = await res.json().catch(() => ({}))
throw new Error(body?.detail ?? `HTTP ${res.status}`)
}
onStatusChange?.()
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
async function handleStart(e) {
e.preventDefault()
await startPipeline(source, lines)
}
async function handleDemo() {
await startPipeline('demo', [DEMO_LINE])
}
async function handleStop() {
setLoading(true)
setError(null)
try {
await fetch(`${API}/pipeline/stop`, { method: 'POST' })
onStatusChange?.()
} catch (err) {
setError(err.message)
} finally {
setLoading(false)
}
}
// ── render ────────────────────────────────────────────────────────────────
return (
<div className="bg-gray-800 border border-gray-700 rounded-xl p-4">
<h3 className="text-sm font-semibold text-gray-300 mb-3">Pipeline Control</h3>
<form onSubmit={handleStart} className="flex flex-wrap gap-3 items-end">
{/* Source */}
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">Source</label>
<input
className="bg-gray-900 border border-gray-600 rounded px-3 py-1.5 text-sm text-gray-200 w-52 focus:outline-none focus:border-blue-500"
placeholder="0 /path/to/video rtsp://…"
value={source}
onChange={e => setSource(e.target.value)}
disabled={isRunning}
/>
</div>
{/* Confidence */}
<div className="flex flex-col gap-1">
<label className="text-xs text-gray-400">Confidence</label>
<input
type="number"
step="0.05"
min="0.05"
max="0.95"
className="bg-gray-900 border border-gray-600 rounded px-3 py-1.5 text-sm text-gray-200 w-24 focus:outline-none focus:border-blue-500"
value={conf}
onChange={e => setConf(e.target.value)}
disabled={isRunning}
/>
</div>
{/* Counting lines toggle */}
{!isRunning && (
<button
type="button"
onClick={() => setShowLines(v => !v)}
className="px-3 py-1.5 rounded border border-gray-600 text-xs text-gray-400 hover:text-gray-200 hover:border-gray-400 transition-colors"
>
{showLines ? 'Hide lines' : `Lines (${lines.length})`}
</button>
)}
{/* Start / Stop buttons */}
{!isRunning ? (
<div className="flex gap-2">
<button
type="submit"
disabled={loading}
className="px-4 py-1.5 rounded bg-blue-600 hover:bg-blue-500 text-sm font-medium disabled:opacity-50 transition-colors"
>
{loading ? 'Starting…' : 'Start'}
</button>
<button
type="button"
onClick={handleDemo}
disabled={loading}
className="px-4 py-1.5 rounded bg-emerald-700 hover:bg-emerald-600 text-sm font-medium disabled:opacity-50 transition-colors"
title="Run synthetic demo — no ONNX model or camera required"
>
Demo
</button>
</div>
) : (
<button
type="button"
onClick={handleStop}
disabled={loading}
className="px-4 py-1.5 rounded bg-red-600 hover:bg-red-500 text-sm font-medium disabled:opacity-50 transition-colors"
>
{loading ? 'Stopping…' : 'Stop'}
</button>
)}
</form>
{/* Counting lines editor */}
{showLines && !isRunning && (
<div className="mt-3 space-y-2">
<div className="text-xs text-gray-500 mb-1">
Counting lines — vehicles are counted when they cross a line
</div>
{lines.map((line, i) => (
<div key={i} className="flex flex-wrap gap-2 items-center">
<input
className="bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 w-20"
placeholder="name"
value={line.name}
onChange={e => updateLine(i, 'name', e.target.value)}
/>
{['x1', 'y1', 'x2', 'y2'].map(f => (
<input
key={f}
type="number"
className="bg-gray-900 border border-gray-700 rounded px-2 py-1 text-xs text-gray-300 w-16"
placeholder={f}
value={line[f]}
onChange={e => updateLine(i, f, e.target.value)}
/>
))}
<button
type="button"
onClick={() => removeLine(i)}
className="text-xs text-red-500 hover:text-red-400"
>
</button>
</div>
))}
{lines.length < 3 && (
<button
type="button"
onClick={addLine}
className="text-xs text-blue-400 hover:text-blue-300"
>
+ Add line
</button>
)}
</div>
)}
{/* CPU-only warning — shown when GPU is confirmed absent and source is not demo */}
{gpu === false && !isRunning && source.trim().toLowerCase() !== 'demo' && (
<div className="mt-3 flex items-start gap-2 rounded-lg border border-yellow-700 bg-yellow-900/30 px-3 py-2 text-xs text-yellow-300">
<span className="mt-0.5 shrink-0"></span>
<span>
<strong>CPU-only mode</strong> — no GPU detected. Real video inference will run at
roughly 3–8 FPS and may lag behind live sources.{' '}
Keep uploaded video clips under <strong>20 seconds</strong> for best results,
or click <strong>Demo</strong> for full-speed synthetic playback.
</span>
</div>
)}
{error && <p className="mt-2 text-xs text-red-400">{error}</p>}
{status && (
<p className="mt-2 text-xs text-gray-500">
{isRunning
? `${status.demo_mode ? '[DEMO] ' : ''}Running — frame ${status.frame_index} · ${status.fps} fps · source: ${status.source}`
: 'Pipeline stopped'}
</p>
)}
</div>
)
}