Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .cspell/general-technical.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1950,10 +1950,17 @@ allclose
cmdk
cooldown
desync
dvwh
dvwm
emanufacturing
faststart
fourcc
midtone
movflags
nameidentifier
persistable
rawvideo
rvfc
srcs
tobytes
ultrafast
17 changes: 17 additions & 0 deletions .github/instructions/dataviewer.instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ Browser tools include: click_element, drag_element, handle_dialog, hover_element
* Elements may make better sense being placed in other places than initially planned, make sure where they're placed and how they're placed makes the most sense.
* Update events captured and viewable by Diagnostics viewer as-needed and as new functionality is added or refactored. When more diagnostics would be better for solving a problem then add it to the Diagnostics viewer.

## Input Sanitization

All user-provided values entering through `@router.` endpoint parameters or `request.` body fields must be sanitized before use:

* Strings, apply `.replace("\r", "").replace("\n", "")` to strip CR/LF characters that enable log injection.
* Numeric types, coerce with `int()`, `float()`, or `bool()` as appropriate (e.g., `int(episode_idx)`, `float(request.confidence)`).
* Sanitize at the earliest point, inside the router endpoint function body before passing values to service methods, logs, or any downstream calls.

CodeQL workaround for logging:

* Keep shared validation and `Depends()`-based sanitization in place.
* When a `logger.` call writes `dataset_id`, `episode_idx`, `frame_idx`, `confidence`, or `model_name`, sanitize or coerce that specific value inline at the log call as well.
* Prefer inline forms such as `dataset_id.replace("\r", "").replace("\n", "")`, `int(episode_idx)`, `int(frame_idx)`, `float(confidence)`, and `model_name.replace("\r", "").replace("\n", "")` so CodeQL can see the transformation on the logged value itself.
* Do not sanitize or wrap the exception as the logger will take care of it.

This can be done with `Depends()` on parameters.

## RPI Agent High Priority Instructions

These instructions take priority over instructions from RPI Agent (rpi-agent.agent.md):
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -465,4 +465,4 @@ AGENTS.md
# Terraform Environments
envs/

src/dataviewer/datasets/
datasets/
14 changes: 14 additions & 0 deletions src/dataviewer/backend/.env.azure.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Local development against Azure Blob Storage (osmorbt3-dev-001 environment)
#
# Usage:
# 1. Copy to .env: cp .env.azure.example .env
# 2. Log in: az login (or source deploy/000-prerequisites/az-sub-init.sh)
# 3. Start: npm run dev:backend (from src/dataviewer/)
#
# Your Azure AD user must have Storage Blob Data Reader (or Contributor) on the account.
# Do NOT set AZURE_CLIENT_ID locally — it overrides CLI credentials with managed identity.

HMI_STORAGE_BACKEND=azure
AZURE_STORAGE_ACCOUNT_NAME=<insert-storage-account-name>
AZURE_STORAGE_DATASET_CONTAINER=datasets
AZURE_STORAGE_ANNOTATION_CONTAINER=annotations
8 changes: 7 additions & 1 deletion src/dataviewer/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,14 @@ HMI_DATA_PATH=../../../datasets
# ─────────────────────────────────────────────────────────────────────────────
# Azure Blob Storage (required when HMI_STORAGE_BACKEND=azure)
# ─────────────────────────────────────────────────────────────────────────────
# Azure Blob Storage (required when HMI_STORAGE_BACKEND=azure)
# ─────────────────────────────────────────────────────────────────────────────
# Prerequisite: run `az login` or `source deploy/000-prerequisites/az-sub-init.sh`
# Your Azure AD user needs Storage Blob Data Reader (or Contributor) on the account.
# Do NOT set AZURE_CLIENT_ID locally — it overrides CLI credentials.
# See .env.azure.example for a ready-to-copy config.

# Azure Storage account name (e.g. mystorageaccount)
# Azure Storage account name
# AZURE_STORAGE_ACCOUNT_NAME=

# Blob container that holds dataset files (videos, parquet, HDF5).
Expand Down
4 changes: 4 additions & 0 deletions src/dataviewer/backend/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ FROM python:3.11-slim AS base

WORKDIR /app

# Install system dependencies (ffmpeg for HDF5 video generation)
RUN apt-get update && apt-get install -y --no-install-recommends ffmpeg \
&& rm -rf /var/lib/apt/lists/*

# Install uv
RUN pip install --no-cache-dir uv

Expand Down
24 changes: 24 additions & 0 deletions src/dataviewer/backend/src/api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import logging
import os
from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager
from pathlib import Path

from dotenv import load_dotenv
Expand All @@ -20,6 +22,10 @@
format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
)

# Suppress verbose Azure SDK HTTP request logging
logging.getLogger("azure").setLevel(logging.WARNING)
logging.getLogger("azure.core.pipeline.policies.http_logging_policy").setLevel(logging.WARNING)

# Load .env before any config or service singletons are initialized so that
# all env vars are available to get_app_config() on first access.
env_path = Path(__file__).parent.parent.parent / ".env"
Expand All @@ -31,10 +37,28 @@

_config = load_config()

logger = logging.getLogger(__name__)


@asynccontextmanager
async def lifespan(_app: FastAPI) -> AsyncGenerator[None]:
"""Clean up blob sync temp directories on shutdown."""
yield
from .services.dataset_service import get_dataset_service

try:
service = get_dataset_service()
service.cleanup_temp_dirs()
logger.info("Cleaned up blob sync temp directories")
except Exception:
Comment thread Fixed
pass # Best-effort cleanup; failure here must not block shutdown


app = FastAPI(
title="LeRobot Annotation API",
description="API for episode annotation in robot demonstration datasets",
version="0.1.0",
lifespan=lifespan,
openapi_tags=[
{"name": "auth", "description": "Authentication utilities"},
],
Expand Down
47 changes: 25 additions & 22 deletions src/dataviewer/backend/src/api/models/annotations.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@

from datetime import datetime
from enum import Enum, StrEnum
from typing import ClassVar

from pydantic import BaseModel, Field
from pydantic import Field

from ..validation import SanitizedModel

# ============================================================================
# Task Completeness Types
Expand All @@ -34,7 +37,7 @@ class ConfidenceLevel(int, Enum):
FIVE = 5


class TaskCompletenessAnnotation(BaseModel):
class TaskCompletenessAnnotation(SanitizedModel):
"""Task completeness annotation with rating and optional details."""

rating: TaskCompletenessRating
Expand All @@ -43,7 +46,7 @@ class TaskCompletenessAnnotation(BaseModel):
failure_reason: str | None = None
subtask_reached: str | None = None

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


# ============================================================================
Expand Down Expand Up @@ -73,25 +76,25 @@ class TrajectoryFlag(StrEnum):
CORRECTION_HEAVY = "correction-heavy"


class TrajectoryQualityMetrics(BaseModel):
class TrajectoryQualityMetrics(SanitizedModel):
"""Individual trajectory quality metrics."""

smoothness: QualityScore
efficiency: QualityScore
safety: QualityScore
precision: QualityScore

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


class TrajectoryQualityAnnotation(BaseModel):
class TrajectoryQualityAnnotation(SanitizedModel):
"""Complete trajectory quality annotation."""

overall_score: QualityScore
metrics: TrajectoryQualityMetrics
flags: list[TrajectoryFlag] = Field(default_factory=list)

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


# ============================================================================
Expand Down Expand Up @@ -129,7 +132,7 @@ class IssueSeverity(StrEnum):
CRITICAL = "critical"


class DataQualityIssue(BaseModel):
class DataQualityIssue(SanitizedModel):
"""Individual data quality issue."""

type: DataQualityIssueType
Expand All @@ -138,16 +141,16 @@ class DataQualityIssue(BaseModel):
affected_streams: list[str] | None = None
notes: str | None = None

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


class DataQualityAnnotation(BaseModel):
class DataQualityAnnotation(SanitizedModel):
"""Complete data quality annotation."""

overall_quality: DataQualityLevel
issues: list[DataQualityIssue] = Field(default_factory=list)

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


# ============================================================================
Expand Down Expand Up @@ -176,7 +179,7 @@ class AnomalyType(StrEnum):
OTHER = "other"


class Anomaly(BaseModel):
class Anomaly(SanitizedModel):
"""Individual anomaly marker."""

id: str
Expand All @@ -188,10 +191,10 @@ class Anomaly(BaseModel):
auto_detected: bool = False
verified: bool = False

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


class AnomalyAnnotation(BaseModel):
class AnomalyAnnotation(SanitizedModel):
"""Container for anomaly annotations."""

anomalies: list[Anomaly] = Field(default_factory=list)
Expand All @@ -202,7 +205,7 @@ class AnomalyAnnotation(BaseModel):
# ============================================================================


class EpisodeAnnotation(BaseModel):
class EpisodeAnnotation(SanitizedModel):
"""Complete annotation for a single episode by one annotator."""

annotator_id: str
Expand All @@ -214,18 +217,18 @@ class EpisodeAnnotation(BaseModel):
notes: str | None = None


class EpisodeConsensus(BaseModel):
class EpisodeConsensus(SanitizedModel):
"""Consensus annotation derived from multiple annotators."""

task_completeness: TaskCompletenessRating
trajectory_score: float = Field(ge=1.0, le=5.0)
data_quality: DataQualityLevel
agreement_score: float = Field(ge=0.0, le=1.0)

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


class EpisodeAnnotationFile(BaseModel):
class EpisodeAnnotationFile(SanitizedModel):
"""Complete annotation file for an episode."""

schema_version: str = "1.0.0"
Expand All @@ -240,7 +243,7 @@ class EpisodeAnnotationFile(BaseModel):
# ============================================================================


class ComputedQualityMetrics(BaseModel):
class ComputedQualityMetrics(SanitizedModel):
"""Computed trajectory quality metrics from auto-analysis."""

smoothness_score: float = Field(ge=0.0, le=1.0)
Expand All @@ -250,7 +253,7 @@ class ComputedQualityMetrics(BaseModel):
correction_count: int = Field(ge=0)


class AutoQualityAnalysis(BaseModel):
class AutoQualityAnalysis(SanitizedModel):
"""Auto-analysis result for an episode."""

episode_index: int = Field(ge=0)
Expand All @@ -259,15 +262,15 @@ class AutoQualityAnalysis(BaseModel):
confidence: float = Field(ge=0.0, le=1.0)
flags: list[TrajectoryFlag] = Field(default_factory=list)

model_config = {"use_enum_values": True}
model_config: ClassVar = {"use_enum_values": True}


# ============================================================================
# Annotation Summary Types
# ============================================================================


class AnnotationSummary(BaseModel):
class AnnotationSummary(SanitizedModel):
"""Aggregated annotation metrics for a dataset."""

dataset_id: str
Expand Down
23 changes: 17 additions & 6 deletions src/dataviewer/backend/src/api/models/detection.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
endpoints and match the frontend TypeScript type definitions.
"""

from pydantic import BaseModel, Field
from pydantic import Field, field_validator

from ..validation import SanitizedModel

class DetectionRequest(BaseModel):

class DetectionRequest(SanitizedModel):
"""Request parameters for running object detection."""

frames: list[int] | None = Field(
Expand All @@ -26,8 +28,17 @@ class DetectionRequest(BaseModel):
description="YOLO model variant: yolo11n, yolo11s, yolo11m, yolo11l, yolo11x",
)

@field_validator("frames")
@classmethod
def validate_frames(cls, frames: list[int] | None) -> list[int] | None:
if frames is None:
return None
if any(frame < 0 for frame in frames):
raise ValueError("Frame indices must be non-negative")
return frames


class Detection(BaseModel):
class Detection(SanitizedModel):
"""Single object detection result."""

class_id: int = Field(ge=0, description="COCO class ID")
Expand All @@ -36,22 +47,22 @@ class Detection(BaseModel):
bbox: tuple[float, float, float, float] = Field(description="Bounding box as (x1, y1, x2, y2) in pixels")


class DetectionResult(BaseModel):
class DetectionResult(SanitizedModel):
"""Detection results for a single frame."""

frame: int = Field(ge=0, description="Frame index")
detections: list[Detection] = Field(default_factory=list)
processing_time_ms: float = Field(ge=0.0, description="Inference time in milliseconds")


class ClassSummary(BaseModel):
class ClassSummary(SanitizedModel):
"""Summary statistics for a detection class."""

count: int = Field(ge=0, description="Total detections of this class")
avg_confidence: float = Field(ge=0.0, le=1.0, description="Average confidence")


class EpisodeDetectionSummary(BaseModel):
class EpisodeDetectionSummary(SanitizedModel):
"""Complete detection results for an episode."""

total_frames: int = Field(ge=0, description="Total frames in episode")
Expand Down
Loading
Loading