Add a comprehensive video review feature to the Final Review page that allows
reviewers to watch videos with caption overlays and add timestamped notes.
Backend:
- New ReviewNote model for MongoDB with job_id, asset_key, timestamp, content
- CRUD API endpoints at /jobs/{job_id}/review-notes
- Owner-only edit/delete permissions (admins can bypass)
- Database indexes for efficient querying
Frontend:
- VideoReviewPlayer component with video player and caption overlay
- NotesSidebar for viewing/adding notes with auto-highlight when video reaches timestamp
- SyncedCaptionList with auto-scroll and click-to-seek
- AssetTabs for switching between languages and accessible videos
- React Query hooks with 30s polling for collaborative updates
Features:
- Notes persist to database and are shared across all reviewers
- Notes highlight for 5 seconds when video playback reaches their timestamp
- Click note to seek video to that position
- Pause video to add note at current timestamp
- Accessible videos use retimed captions when available
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
56 lines
1.9 KiB
Python
56 lines
1.9 KiB
Python
"""Pydantic schemas for Review Note API requests and responses."""
|
|
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
class ReviewNoteCreateRequest(BaseModel):
|
|
"""Request body for creating a new review note."""
|
|
|
|
asset_key: str = Field(..., description="Asset identifier (e.g., 'en', 'es_accessible')")
|
|
timestamp_seconds: float = Field(..., ge=0, description="Video timestamp in seconds")
|
|
content: str = Field(..., min_length=1, max_length=2000, description="Note content")
|
|
|
|
|
|
class ReviewNoteUpdateRequest(BaseModel):
|
|
"""Request body for updating an existing review note."""
|
|
|
|
content: str = Field(..., min_length=1, max_length=2000, description="Updated note content")
|
|
|
|
|
|
class ReviewNoteResponse(BaseModel):
|
|
"""Response model for a single review note."""
|
|
|
|
id: str
|
|
job_id: str
|
|
asset_key: str
|
|
timestamp_seconds: float
|
|
content: str
|
|
user_id: str
|
|
user_name: str
|
|
created_at: str # ISO format
|
|
updated_at: Optional[str] = None # ISO format
|
|
|
|
@classmethod
|
|
def from_model(cls, note: dict) -> "ReviewNoteResponse":
|
|
"""Create a response from a MongoDB document."""
|
|
return cls(
|
|
id=str(note.get("_id", note.get("id"))),
|
|
job_id=note["job_id"],
|
|
asset_key=note["asset_key"],
|
|
timestamp_seconds=note["timestamp_seconds"],
|
|
content=note["content"],
|
|
user_id=note["user_id"],
|
|
user_name=note["user_name"],
|
|
created_at=note["created_at"].isoformat() if isinstance(note["created_at"], datetime) else note["created_at"],
|
|
updated_at=note["updated_at"].isoformat() if note.get("updated_at") and isinstance(note["updated_at"], datetime) else note.get("updated_at"),
|
|
)
|
|
|
|
|
|
class ReviewNotesListResponse(BaseModel):
|
|
"""Response model for a list of review notes."""
|
|
|
|
notes: list[ReviewNoteResponse]
|
|
total: int
|