Files
Centralised_VideoVerificati…/server/src/models.rs
2026-07-07 22:42:47 +05:30

508 lines
15 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use serde::{Deserialize, Serialize};
use serde_json::Value;
// ---- Parsed from the sibling export-JSON (video-annotator export format) ----
#[derive(Deserialize, Default)]
pub struct ExportDoc {
#[serde(default)]
pub video: Option<ExportVideo>,
#[serde(default)]
pub fixed_annotations: Vec<FixedAnn>,
#[serde(default)]
pub range_annotations: Vec<RangeAnn>,
}
#[derive(Deserialize, Default)]
pub struct ExportVideo {
#[serde(default)]
pub file_name: String,
#[serde(default)]
pub width: Option<i64>,
#[serde(default)]
pub height: Option<i64>,
#[serde(default)]
pub fps: Option<f64>,
#[serde(default)]
pub frame_count: Option<i64>,
#[serde(default)]
pub annotation_time_ms: i64,
}
#[derive(Deserialize)]
pub struct FixedAnn {
#[serde(default)]
pub label: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub frame_number: i64,
#[serde(default, rename = "type")]
pub shape_type: String,
#[serde(default)]
pub vertices: Value,
#[serde(default)]
pub annotated_by: String,
#[serde(default)]
pub remark: String,
#[serde(default)]
pub review_status: String,
#[serde(default)]
pub subclass: String,
#[serde(default)]
pub created_at: String,
}
#[derive(Deserialize)]
pub struct RangeAnn {
#[serde(default)]
pub label: String,
#[serde(default)]
pub side: String,
#[serde(default)]
pub start_frame: i64,
#[serde(default)]
pub start_vertices: Value,
#[serde(default)]
pub end_frame: Option<i64>,
#[serde(default)]
pub end_vertices: Option<Value>,
#[serde(default, rename = "type")]
pub shape_type: String,
#[serde(default)]
pub annotated_by: String,
#[serde(default)]
pub remark: String,
#[serde(default)]
pub review_status: String,
#[serde(default)]
pub subclass: String,
#[serde(default)]
pub created_at: String,
}
// ---- API response shapes ----
#[derive(Serialize)]
pub struct ProjectSummary {
pub id: i32,
pub name: String,
pub source_path: String,
pub source_kind: String,
pub client_id: Option<i32>,
pub client_name: String,
pub total: i64,
pub pending: i64,
pub annotated: i64,
pub verified: i64,
}
/// A client owning one or more projects (admin-created from the dashboard).
#[derive(Serialize)]
pub struct ClientRow {
pub id: i32,
pub name: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub project_count: i64,
}
/// Admin: `POST /api/clients` — create a client.
#[derive(Deserialize)]
pub struct NewClientRequest {
pub name: String,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct VideoRow {
pub id: i32,
pub file_name: String,
pub rel_path: String,
pub has_json: bool,
pub width: Option<i32>,
pub height: Option<i32>,
pub fps: Option<f64>,
pub frame_count: Option<i32>,
pub annotation_count: i32,
// Baseline count this video started a verification pass with: the NAS-ingested
// count, or (after a push) the count the worker had at claim time. Compared with
// annotation_count it shows how many the worker added/deleted.
pub imported_count: i32,
pub annotation_time_ms: i64,
pub primary_annotator: String,
pub status: String,
pub annotated_at: Option<chrono::DateTime<chrono::Utc>>,
// Phase 3: claim/verify dimension (orthogonal to `status`).
pub claimed_by: Option<String>,
pub claimed_at: Option<chrono::DateTime<chrono::Utc>>,
pub lease_expires_at: Option<chrono::DateTime<chrono::Utc>>,
pub completed_by: Option<String>,
pub completed_at: Option<chrono::DateTime<chrono::Utc>>,
pub verify_time_ms: i64,
// Annotations still flagged for review (review_status='flagged') on this video.
pub review_count: i64,
// How many times pushed (1 = verified, ≥2 = re-verified) + the distinct verifiers
// ("ravi, john"), both derived from the push event log.
pub verify_count: i64,
pub verifiers: String,
// Admin assignment (advisory owner) + the derived lifecycle label for the UI badge:
// pending | annotated | assigned | in-progress | verified | re-verified.
pub assigned_to: Option<String>,
pub workflow_status: String,
// Road type label ('' = unassigned) — injected into the export/claim JSON.
pub road_type: String,
// GPS pipeline: pending | ok | none | error (+ measured track length / last error).
pub gps_status: String,
pub gps_len_m: Option<f64>,
pub gps_error: Option<String>,
// Data quality: raw fix count + biggest dropout — the table compares the count
// against the video duration (≈1 fix/s) and flags mismatches.
pub gps_sample_count: Option<i32>,
pub gps_max_gap_s: Option<f64>,
// Real container duration from the scan (raw NAS videos have no fps/frame_count).
pub duration_s: Option<f64>,
// Client-calibrated chainage + the immutable GPS-measured (sr) chainage, meters.
pub chainage_start_m: Option<f64>,
pub chainage_end_m: Option<f64>,
pub sr_chainage_start_m: Option<f64>,
pub sr_chainage_end_m: Option<f64>,
pub route_seq: Option<i32>,
// "Raise hand" signal — anyone may raise (to start a conversation) or lower it.
pub hand_raised: bool,
pub hand_raised_by: Option<String>,
pub hand_raised_at: Option<chrono::DateTime<chrono::Utc>>,
// "Ignore" flag — admin-only; the UI crosses out ignored rows.
pub ignored: bool,
pub ignored_by: Option<String>,
// Remark thread (json array of {username, body, created_at}); '[]' when none.
pub remarks: serde_json::Value,
// Per-pass annotation-count history (json array of {username, at, count}, one per
// push in order); '[]' when never pushed. Drives the imported→verify→re-verify
// count breakdown in the UI.
pub passes: serde_json::Value,
}
/// One row of `GET /api/projects/:id/map` — a video as the map sees it. `track` is
/// the (transport-downsampled) effective polyline: corrected when kept, else raw;
/// None until the GPS sweep has scanned the video (or when it has no metadata).
#[derive(Serialize, sqlx::FromRow)]
pub struct MapVideo {
pub id: i32,
pub file_name: String,
pub rel_path: String,
pub status: String,
pub workflow_status: String,
pub ignored: bool,
pub hand_raised: bool,
pub assigned_to: Option<String>,
pub claimed_by: Option<String>,
pub annotation_count: i32,
pub road_type: String,
pub gps_status: String,
pub gps_len_m: Option<f64>,
pub gps_error: Option<String>,
pub chainage_start_m: Option<f64>,
pub chainage_end_m: Option<f64>,
pub sr_chainage_start_m: Option<f64>,
pub sr_chainage_end_m: Option<f64>,
pub route_seq: Option<i32>,
pub remark_count: i64,
pub track: Option<Value>,
pub corrected: bool,
// Who/what produced the correction ('auto' or a username) + its summary note.
pub gps_corrected_by: Option<String>,
pub gps_corrected_note: Option<String>,
// Data quality from the raw scan: fix count + biggest dropout between fixes.
pub gps_sample_count: Option<i32>,
pub gps_max_gap_s: Option<f64>,
}
/// `GET /api/projects/:id/road_types` — a label + how many videos carry it.
#[derive(Serialize, sqlx::FromRow)]
pub struct RoadTypeRow {
pub name: String,
pub video_count: i64,
}
/// `POST /api/projects/:id/road_types {names}` — replace the label set (admin).
#[derive(Deserialize)]
pub struct RoadTypesRequest {
pub names: Vec<String>,
}
/// `POST /api/projects/:id/road_type_assign {video_ids, road_type}` — bulk-assign.
#[derive(Deserialize)]
pub struct RoadTypeAssignRequest {
pub video_ids: Vec<i32>,
pub road_type: String, // '' = clear
}
/// `POST /api/videos/:id/hand {raised}` — raise/lower the hand on a video.
#[derive(Deserialize)]
pub struct HandRequest {
pub raised: bool,
}
/// `POST /api/videos/:id/ignore {ignored}` — admin-only ignore toggle.
#[derive(Deserialize)]
pub struct IgnoreRequest {
pub ignored: bool,
}
#[derive(Deserialize)]
pub struct AssignRequest {
pub video_ids: Vec<i32>,
pub assignee: String, // empty = unassign
}
/// A user picked for a project (project_members joined with users).
#[derive(Serialize, sqlx::FromRow)]
pub struct MemberRow {
pub username: String,
pub display_name: String,
pub role: String,
pub added_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct AddMemberRequest {
pub username: String,
}
/// One line in a video's remark thread.
#[derive(Serialize, sqlx::FromRow)]
pub struct Remark {
pub username: String,
pub body: String,
pub created_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct NewRemarkRequest {
pub body: String,
}
#[derive(Serialize)]
pub struct Overview {
pub total: i64,
pub pending: i64,
pub annotated: i64,
pub verified: i64,
pub pct_done: f64,
}
#[derive(Serialize)]
pub struct LeaderRow {
pub user: String,
pub videos: i64,
pub annotations: i64,
pub total_time_ms: i64,
pub avg_time_ms: i64,
}
#[derive(Serialize)]
pub struct Stats {
pub overview: Overview,
pub leaderboard: Vec<LeaderRow>,
// Phase 3: verification leaderboard (by `completed_by`).
pub verifiers: Vec<VerifierRow>,
// Project time, verification pace + worker-based ETA.
pub timeline: ProjectTime,
}
/// Aggregate project time metrics + a worker-parallelised ETA for the remaining work.
#[derive(Serialize)]
pub struct ProjectTime {
/// Users picked for the project (the parallelism factor for the ETA).
pub active_workers: i64,
/// Average effort per verified video (annotation + verify time).
pub avg_video_ms: i64,
/// Total time spent by all users across all videos (annotation + verify).
pub total_spent_ms: i64,
/// Wall-clock from project inception to last completion (or now if ongoing).
pub elapsed_ms: i64,
/// Whether every video is verified.
pub all_verified: bool,
/// Estimated remaining work time = remaining × avg_video ÷ active_workers.
pub eta_ms: Option<i64>,
/// Whether the project timer is currently running (accumulating).
pub time_running: bool,
/// Last verify/re-verify activity (drives the 72h auto-stop).
pub last_activity_at: Option<chrono::DateTime<chrono::Utc>>,
/// Verification pace: push (verify) events in the recent window, and the window
/// length in days. videos/day = recent_verified ÷ recent_window_days.
pub recent_verified: i64,
pub recent_window_days: i64,
}
/// `POST /api/projects/:id/timer {action}` — start | stop | reset (admin only).
#[derive(Deserialize)]
pub struct TimerRequest {
pub action: String,
}
/// `POST /api/projects/:id/folders {folders}` — set the folders to consider (admin).
#[derive(Deserialize)]
pub struct FoldersRequest {
pub folders: Vec<String>,
}
/// `GET /api/projects/:id/folders` — a folder in the project + its video count +
/// whether it's currently included in verification.
#[derive(Serialize, sqlx::FromRow)]
pub struct FolderRow {
pub folder: String,
pub video_count: i64,
pub included: bool,
}
#[derive(Serialize)]
pub struct VerifierRow {
pub user: String,
pub videos: i64,
pub total_time_ms: i64,
pub avg_time_ms: i64,
/// Total annotations this user authored (annotated_by) in the project.
pub annotations: i64,
/// Annotations added per minute of work (annotations ÷ total_time). This is the
/// ranking metric: "most annotations in the least time". 0 when no time/annotations.
pub annotations_per_min: f64,
/// 1-based rank by annotations-per-minute (highest first); 0 = not yet ranked
/// (no annotations or no recorded time).
pub rank: i64,
}
// ---- Phase 3: auth + worker pull/push shapes ----
/// `GET /api/auth/whoami` — identifies the bearer.
#[derive(Serialize)]
pub struct WhoAmI {
pub username: String,
pub role: String,
}
/// `POST /api/videos/:id/claim` response — everything the desktop needs to verify.
#[derive(Serialize)]
pub struct ClaimResponse {
pub video_id: i32,
pub file_name: String,
pub rel_path: String,
pub width: Option<i32>,
pub height: Option<i32>,
pub fps: Option<f64>,
pub frame_count: Option<i32>,
pub lease_expires_at: chrono::DateTime<chrono::Utc>,
pub download_url: String,
/// Full preloaded export doc (`{video, fixed_annotations[], range_annotations[]}`)
/// for the desktop to import via its existing import path.
pub annotations: Value,
}
/// Admin: `POST /api/users` — provision a worker/admin.
#[derive(Deserialize)]
pub struct NewUserRequest {
pub username: String,
#[serde(default)]
pub display_name: String,
#[serde(default = "default_role")]
pub role: String,
}
fn default_role() -> String {
"ml_support".into()
}
/// Admin: `POST /api/users/:id/update` — edit a user's display name + role.
#[derive(Deserialize)]
pub struct UpdateUserRequest {
#[serde(default)]
pub display_name: String,
#[serde(default)]
pub role: String,
}
/// Admin user-provision response — the **only** time the clear token is shown.
#[derive(Serialize)]
pub struct NewUserResponse {
pub username: String,
pub display_name: String,
pub role: String,
pub token: String,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct UserRow {
pub id: i32,
pub username: String,
pub display_name: String,
pub role: String,
pub active: bool,
pub created_at: chrono::DateTime<chrono::Utc>,
}
/// `POST /api/projects` — create a project under a client (any authenticated user).
#[derive(Deserialize)]
pub struct NewProjectRequest {
pub client_id: i32,
pub name: String,
pub source_path: String,
/// 'local' (default) or 'nfs'.
#[serde(default = "default_source_kind")]
pub source_kind: String,
}
fn default_source_kind() -> String {
"local".into()
}
/// `POST /api/projects/:id/source` — change a project's sync directory (any authenticated user).
#[derive(Deserialize)]
pub struct UpdateSourceRequest {
pub source_path: String,
#[serde(default = "default_source_kind")]
pub source_kind: String,
}
/// `GET /api/admin/storage` — DB size + row counts (admin only).
#[derive(Serialize)]
pub struct StorageInfo {
pub db_bytes: i64,
pub videos: i64,
pub annotations: i64,
pub video_events: i64,
pub audit_events: i64,
/// Retention cap applied nightly to the event/audit logs.
pub max_log_rows: i64,
}
/// `GET /api/audit` — an org-level audit row.
#[derive(Serialize, sqlx::FromRow)]
pub struct AuditRow {
pub at: chrono::DateTime<chrono::Utc>,
pub username: String,
pub action: String,
pub detail: Value,
}
/// `GET /api/projects/:id/activity` — live claims + recent events.
#[derive(Serialize)]
pub struct Activity {
pub active_claims: Vec<ActiveClaim>,
pub recent_events: Vec<EventRow>,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct ActiveClaim {
pub video_id: i32,
pub file_name: String,
pub claimed_by: String,
pub claimed_at: chrono::DateTime<chrono::Utc>,
pub lease_expires_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize, sqlx::FromRow)]
pub struct EventRow {
pub video_id: i32,
pub file_name: String,
pub username: String,
pub event: String,
pub at: chrono::DateTime<chrono::Utc>,
}