"""Pydantic schemas for the Chrysopedia API. Read-only schemas for list/detail endpoints and input schemas for creation. Each schema mirrors the corresponding SQLAlchemy model in models.py. """ from __future__ import annotations import uuid from datetime import datetime from pydantic import BaseModel, ConfigDict, Field # ── Health ─────────────────────────────────────────────────────────────────── class HealthResponse(BaseModel): status: str = "ok" service: str = "chrysopedia-api" version: str = "0.1.0" database: str = "unknown" # ── Creator ────────────────────────────────────────────────────────────────── class CreatorBase(BaseModel): name: str slug: str genres: list[str] | None = None folder_name: str class CreatorCreate(CreatorBase): pass class CreatorRead(CreatorBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID view_count: int = 0 created_at: datetime updated_at: datetime class CreatorDetail(CreatorRead): """Creator with nested video count.""" video_count: int = 0 # ── SourceVideo ────────────────────────────────────────────────────────────── class SourceVideoBase(BaseModel): filename: str file_path: str duration_seconds: int | None = None content_type: str transcript_path: str | None = None class SourceVideoCreate(SourceVideoBase): creator_id: uuid.UUID class SourceVideoRead(SourceVideoBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID creator_id: uuid.UUID processing_status: str = "pending" created_at: datetime updated_at: datetime # ── TranscriptSegment ──────────────────────────────────────────────────────── class TranscriptSegmentBase(BaseModel): start_time: float end_time: float text: str segment_index: int topic_label: str | None = None class TranscriptSegmentCreate(TranscriptSegmentBase): source_video_id: uuid.UUID class TranscriptSegmentRead(TranscriptSegmentBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID source_video_id: uuid.UUID # ── KeyMoment ──────────────────────────────────────────────────────────────── class KeyMomentBase(BaseModel): title: str summary: str start_time: float end_time: float content_type: str plugins: list[str] | None = None raw_transcript: str | None = None class KeyMomentCreate(KeyMomentBase): source_video_id: uuid.UUID technique_page_id: uuid.UUID | None = None class KeyMomentRead(KeyMomentBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID source_video_id: uuid.UUID technique_page_id: uuid.UUID | None = None review_status: str = "pending" created_at: datetime updated_at: datetime # ── TechniquePage ──────────────────────────────────────────────────────────── class TechniquePageBase(BaseModel): title: str slug: str topic_category: str topic_tags: list[str] | None = None summary: str | None = None body_sections: dict | None = None signal_chains: list | None = None plugins: list[str] | None = None class TechniquePageCreate(TechniquePageBase): creator_id: uuid.UUID source_quality: str | None = None class TechniquePageRead(TechniquePageBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID creator_id: uuid.UUID source_quality: str | None = None view_count: int = 0 review_status: str = "draft" created_at: datetime updated_at: datetime # ── RelatedTechniqueLink ───────────────────────────────────────────────────── class RelatedTechniqueLinkBase(BaseModel): source_page_id: uuid.UUID target_page_id: uuid.UUID relationship: str class RelatedTechniqueLinkCreate(RelatedTechniqueLinkBase): pass class RelatedTechniqueLinkRead(RelatedTechniqueLinkBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID # ── Tag ────────────────────────────────────────────────────────────────────── class TagBase(BaseModel): name: str category: str aliases: list[str] | None = None class TagCreate(TagBase): pass class TagRead(TagBase): model_config = ConfigDict(from_attributes=True) id: uuid.UUID # ── Transcript Ingestion ───────────────────────────────────────────────────── class TranscriptIngestResponse(BaseModel): """Response returned after successfully ingesting a transcript.""" video_id: uuid.UUID creator_id: uuid.UUID creator_name: str filename: str segments_stored: int processing_status: str is_reupload: bool # ── Pagination wrapper ─────────────────────────────────────────────────────── class PaginatedResponse(BaseModel): """Generic paginated list response.""" items: list = Field(default_factory=list) total: int = 0 offset: int = 0 limit: int = 50 # ── Review Queue ───────────────────────────────────────────────────────────── class ReviewQueueItem(KeyMomentRead): """Key moment enriched with source video and creator info for review UI.""" video_filename: str creator_name: str class ReviewQueueResponse(BaseModel): """Paginated response for the review queue.""" items: list[ReviewQueueItem] = Field(default_factory=list) total: int = 0 offset: int = 0 limit: int = 50 class ReviewStatsResponse(BaseModel): """Counts of key moments grouped by review status.""" pending: int = 0 approved: int = 0 edited: int = 0 rejected: int = 0 class MomentEditRequest(BaseModel): """Editable fields for a key moment.""" title: str | None = None summary: str | None = None start_time: float | None = None end_time: float | None = None content_type: str | None = None plugins: list[str] | None = None class MomentSplitRequest(BaseModel): """Request to split a moment at a given timestamp.""" split_time: float class MomentMergeRequest(BaseModel): """Request to merge two moments.""" target_moment_id: uuid.UUID class ReviewModeResponse(BaseModel): """Current review mode state.""" review_mode: bool class ReviewModeUpdate(BaseModel): """Request to update the review mode.""" review_mode: bool