# UI Specification — Dataset Enrichment Tool **Purpose of this document.** It describes every screen of the existing frontend so the UI can be rebuilt on another platform without losing behaviour. It is written framework-agnostically: it says *what a screen holds, what it does, and what it calls*, not how React does it. Where a detail is load-bearing — removing it breaks the pipeline or silently corrupts training data — it is marked. **Read this together with:** `./requirements.md` (the numbered `REQ-xxx` this serves) and `./design.md` (backend, disk layout, job flows). This file never contradicts them; if it does, `design.md` wins and this file is the bug. **Sections** 1. [What the app is](#1-what-the-app-is) 2. [Global conventions](#2-global-conventions) 3. [Application shell](#3-application-shell) 4. [Page specifications](#4-page-specifications) 5. [Review editor — deep specification](#5-review-editor--deep-specification) 6. [Simplification rules](#6-simplification-rules) 7. [API surface index](#7-api-surface-index) 8. [Redesign brief — prompt siap-pakai](#8-redesign-brief--prompt-siap-pakai) --- ## 1. What the app is A **dense internal tool** for one operator, running on one machine with one GPU. It turns CCTV recordings into a YOLO training dataset and then trains and scores models against it. The pipeline is linear and the UI exists to walk it: ``` Video Archive → Trim → Batch (frames) → Auto-annotate → Review → Data Prep → Dataset → Train → Evaluate ① ② ③ ④ ⑤ ⑥ ⑦ ⑧ ⑨ ``` Each arrow is a page. A user who cannot follow that order in the UI cannot use the app, so **the order of the nav must survive any redesign.** It is not a landing page, not a SaaS dashboard, not multi-tenant. There is no login, no onboarding, no marketing surface. Sessions are long (hours of frame review), so density and keyboard reach beat whitespace and animation. --- ## 2. Global conventions ### 2.1 Routing Hash-based, no router library. `#/` + path, one optional query string. | Route | Screen | Notes | |---|---|---| | `#/projects` | Projects | default when hash is empty | | `#/projects/{id}` | Video Archive (Library) | | | `#/projects/{id}/trim/{encodedRel}` | Trim | `rel` is URI-encoded, e.g. `2026-03-01%2Fbatch4.mp4` | | `#/projects/{id}/batches` | Batches | | | `#/projects/{id}/review?batch={batchId}` | Review | `batch` optional → falls back to first batch | | `#/batches/{batchId}` | Review | alias; batch id in the path | | `#/projects/{id}/data-prep?batches=1,2,3` | Data Preparation | `batches` = the merge selection | | `#/projects/{id}/datasets` | Datasets | | | `#/projects/{id}/models` | Models & Training | | | `#/projects/{id}/live-count` | Live Counting | | | `#/projects/{id}/counting-bench` | Counting Accuracy | | | `#/sam3-playground` | SAM3 Playground | no project context | Unknown paths fall back to Projects. `?batches=` is parsed as comma-separated positive integers; anything else is dropped. ### 2.2 Design tokens Dark by default; light is a toggle stored in `localStorage` under `theme` and applied as `data-theme` on the root element. ``` --bg #0b0f19 page ground --panel rgba(17,24,39,0.5) --panel-raised rgba(30,41,59,0.7) --border rgba(255,255,255,0.12) --text #f3f4f6 --text-muted #9ca3af --text-faint #6b7280 --accent #a855f7 (purple; hover #c084fc) --ok #10b981 --warn #f59e0b --danger #ef4444 --radius 12px (--radius-sm 8px) --space 8px --font Inter, system-ui --mono ui-monospace, JetBrains Mono, Menlo --transition 160ms cubic-bezier(.4,0,.2,1) ``` Semantic colours used inline throughout, and they carry meaning — keep them: | Colour | Means | |---|---| | `#4ade80` green | kept / approved / positive exemplar / better metric | | `#f87171` red | dropped / rejected / negative exemplar / worse metric | | `#fbbf24` amber | needs checking (untrusted clock, held-back frame, unsaved) | | `#38bdf8` blue | selection, current marquee, "this run sees" figures | | `#c084fc` purple | machine work in progress (jobs, SAM3, auto-annotation) | ### 2.3 Class colours — data, not decoration ``` classColor(classId) = ["#f59e0b","#38bdf8","#10b981","#facc15", "#6366f1","#f97316","#ec4899","#9ca3af"][classId % 8] ``` The same class index must render the same hue in **every** place it appears: filmstrip badge, canvas stroke, class chip swatch, project card tag, shape list, crop grid border. A palette change is fine; per-page palettes are not. ### 2.4 Geometry All shape coordinates are **normalized 0–1** against the frame, everywhere, in both directions over the wire. ``` bbox { "type": "bbox", "points": [x0, y0, x1, y1] } polygon { "type": "polygon", "points": [[x,y], [x,y], …] } ``` Two exceptions, both deliberate: - **Exemplars sent to `/batches/{id}/preview`** use SAM3's own format: `[cx, cy, w, h]`, still normalized. - **Exemplars sent to `/frames/{id}/exemplar-label`** use `[x0, y0, x1, y1]` (the backend converts). Do not "unify" these; the backend contract differs per endpoint. The overlay SVG in the auto-annotate modals uses a `0 0 10000 10000` viewBox with `preserveAspectRatio="none"` — coordinates are multiplied by 10000. The review canvas instead uses the frame's real pixel dimensions as its viewBox. Both work; both must keep `preserveAspectRatio="none"` and be sized to a wrapper that hugs the image exactly. ### 2.5 Jobs and progress Every long operation is a server-side job. There is **one worker thread and one GPU**, so jobs queue; the UI never assumes parallelism. ``` job = { id, project_id, batch_id, type, status, progress, total, message, error, log[] } type = extract | autolabel | merge | train | (counting bench, clock scan) status = queued | running | done | failed | cancelled ``` Polling rules as implemented: | Screen | Interval | Condition | |---|---|---| | Shell health badge | 3 s | always | | Library / Batches | 2 s | only while ≥1 job is queued/running | | Review | 2 s | only while this batch has an active job | | Trim | 1 s | only while the extraction job is unfinished | | Models | 2 s | only while the training job is unfinished | | Live Count status | 1 s | always (session may start at any time) | | Counting Bench | 2 s | only while a count or clock-scan job runs | When the active-job count drops from >0 to 0, the page **reloads its data once** — that is how new frames, new shapes and new model versions appear without a manual refresh. Progress UI must always show `progress/total`, a bar, and a cancel affordance. A bare spinner is not acceptable for anything that can run for minutes. ### 2.6 Errors, empty states, confirmations - Backend errors arrive as `{"detail": "..."}`. The UI shows **that message**, never "Error 500". A page that failed its first load renders the error banner *instead of* the page; an error after load renders a dismissible banner *above* the page. - Empty states are sentences that name the next action, e.g. *"No extracted batches in this project yet. Go to Video Archive to trim frames into batches."* - Destructive actions confirm, and the confirmation **enumerates the consequences**. Deleting a class says how many shapes die and how many label files get rewritten. Keep that specificity; a generic "Are you sure?" is a downgrade. --- ## 3. Application shell A fixed 48 px **top bar**, full-height content area below it, page never scrolls horizontally. **Left** — wordmark "Dataset Enrichment", clicking goes to `#/projects`. **Centre** — the nav, in pipeline order. Every project-scoped link uses the current project id, falling back to `route.projectId` and then `1`: 1. Projects — `#/projects` 2. Video Archive — `#/projects/{id}` 3. Batches — `#/projects/{id}/batches` *(also active on Review)* 4. Data Preparation — `#/projects/{id}/data-prep` 5. Datasets — `#/projects/{id}/datasets` 6. Models & Training — `#/projects/{id}/models` 7. Live Counting — `#/projects/{id}/live-count` 8. Counting Accuracy — `#/projects/{id}/counting-bench` 9. SAM3 Playground — `#/sam3-playground` **Right** — health badges from `GET /api/health`, polled every 3 s, plus the theme toggle: ``` GPU: | CPU if none VRAM: GB | N/A SAM3: Ready | Off ← green when ready ``` Icons are SVG (Heroicons/Lucide-style), never emoji. *(Some emoji survive inside page bodies — `🔄 Reset Auto`, `🏷️ Next Shape`, `📋 Copy Prev`, `🚀 Track 5`, `🤖`, `📦`, `⚡`, `🖼️`. These are legacy and **should** become SVG icons in a rebuild; that is the one place the current UI is off-spec.)* An **error boundary** wraps the page area, keyed by route, and renders a "View Exception Caught" card with the error text and a reload button. Keep it: a crash in one page must not blank the shell. --- ## 4. Page specifications Each page below lists: what it is for, what it loads, its layout, its interactions and states, and its endpoints. --- ### 4.1 Projects — `#/projects` **Purpose.** One project = one model you are improving: its base weights, its classes, its dataset. This is the only place a project is created or deleted. **Loads on mount.** `GET /api/projects` → `{ projects: [...], video_root_default: "/videos" }`. Project shape: ``` { id, name, label_type: "bbox"|"polygon", label_type_locked: bool, base_model_path, secondary_model_path, base_model_fallback: "yolo11n.pt", video_root, batch_count, dataset: { train, val }, classes: [ { class_id, name, prompt, annotation_count } ] } ``` **Layout.** Page head (title + one-line description + "New project" button) → optional create-form panel → responsive card grid of projects. **New project form** (inline panel, replaces the button while open): | Field | Control | Rules | |---|---|---| | Name | text, autofocus, required | | | Label type | select: `bbox` (YOLO detect) / `polygon` (YOLO segment) | hint: *fixed once the first batch is merged* | | Val split — every Nth frame | number 0–50, default 5 | | | Video archive root | text, required, default from `video_root_default` | hint: layout `/.mp4`; written only by user-initiated uploads and date folders (REQ-178), nothing else | | Classes | repeatable rows: swatch + name + SAM3 prompt + remove | at least one row; blank names are dropped on submit; prompt defaults to the name | Actions: *Create project* (submits), *Cancel*, *Add class*. **Project card** shows: name, a tag reading `bbox` / `polygon` or `locked: ` (with a tooltip explaining the lock), then a definition list: - **Base models** — "N model(s) inserted" plus which slots are filled (Model 1 Primary / Model 2 Secondary), or "none — default (yolo11n.pt)". - **Archive** — the video root, monospace. - **Batches** — count. - **Dataset** — `X train / Y val`, or "empty". Then the **class chips**: swatch + name + annotation count + an `×` delete button (hidden when only one class remains). Only the first 8 render; the rest sit behind a `+N more` toggle, because a base model can carry 80 classes. A `+ add class` chip opens an inline two-field form (name + optional prompt). Card actions: **Open** (→ Video Archive), **Model 1** upload (`.pt`), **Model 2** upload (`.pt`), and a danger delete. **Confirmations.** - Delete class: lists shapes to be deleted, label files to be rewritten, and warns that classes above it are renumbered. *This renumbering is the whole point — see §6.* - Delete project: "Delete X and everything under it?" **Endpoints.** `GET /projects`, `POST /projects`, `DELETE /projects/{id}`, `POST /projects/{id}/classes`, `DELETE /projects/{id}/classes/{classId}`, `POST /projects/{id}/base-model`, `POST /projects/{id}/secondary-model`. --- ### 4.2 Video Archive (Library) — `#/projects/{id}` **Purpose.** Browse the read-only recording archive, grouped into **cycles**, and pick a video to trim. Also hosts the batch table for this project. **The cycle concept — load-bearing domain logic.** A production cycle runs **06:00 to 05:59 the next morning**, so it always straddles midnight and covers two calendar dates. It is named after the date it *starts* (`Siklus 3 Mar 2026`). Grouping comes from the **timestamp burned into the video image**, not from the folder name — a recording made at 00:07 belongs to the cycle that began the previous morning. Files on disk are never moved or renamed; when a file's folder disagrees with its cycle, the folder date is shown beside it in amber. **Loads on mount** (parallel): `GET /projects/{id}`, `GET /projects/{id}/archive/cycles`, `GET /projects/{id}/batches`, `GET /jobs?project_id={id}`. Then, whenever the selected cycle changes: `GET /projects/{id}/archive/cycles/{cycle}`. **Layout.** ``` ┌ page head: "Video Archive" + archive path + cycle explainer ──── [Cek truk (v4)] ┐ ├ ActiveJobsBanner (only when jobs are running) ───────────────────────────────────┤ ├ cycle list (left rail) │ video table (right, with filter box) ───────────────────┤ ├ Batch table (BatchList) ─────────────────────────────────────────────────────────┤ ``` **Cycle rail.** One button per cycle: folder icon, `Siklus D Mon YYYY`, an amber dot if any recording's clock is unverified, and the video count on the right. `aria-current` marks the selection. **Video table** columns: `Batch` (running number within the cycle), `Direkam` (recorded time `hh:mm:ss`, amber when the clock is untrusted), `File`, `Duration`, `Resolution`, `FPS`, `Size`, `Truk`, `Status`, and a **Trim** action. - *Truk* shows `hits/samples` in green if a v4 truck-scan found trucks, "tanpa truk" in red if it found none, "belum dicek" if never scanned. - *Status* shows `N batches` in blue if this video has already been trimmed, else "Unused". - Trim is disabled when `ffprobe` could not read the duration. - A text box filters rows by batch label, client-side. **Cek truk (v4)** posts `POST /projects/{id}/archive/truck-scan` — samples 12 frames per recording with the latest model to check a truck is actually present. **ActiveJobsBanner** (shared with Batches): one row per running job — status dot, capitalised type, status, `progress/total`, Cancel, a progress bar, and the last log line. **BatchList** (shared with Batches; see §4.4). **Endpoints.** `GET /projects/{id}`, `/archive/cycles`, `/archive/cycles/{cycle}`, `/batches`, `/jobs`, `POST /archive/truck-scan`, `POST /jobs/{id}/cancel`. --- ### 4.3 Trim — `#/projects/{id}/trim/{rel}` **Purpose.** Choose an in/out range and a sampling rate, then extract frames into a new batch. **Loads on mount.** `GET /projects/{id}/video/info?rel=…` → `{ date_label, batch_label, duration, width, height, fps }`. End defaults to `min(duration, 60)` seconds, start to 0, fps to 1. **Layout.** Two columns: a `