"""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 typing import Literal 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 content_hash: str | None = None processing_status: str = "not_started" 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 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: list | dict | None = None body_sections_format: str = "v1" 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 creator_name: str = "" creator_slug: str = "" source_quality: str | None = None view_count: int = 0 key_moment_count: int = 0 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 content_hash: str # ── Pagination wrapper ─────────────────────────────────────────────────────── class PaginatedResponse(BaseModel): """Generic paginated list response.""" items: list = Field(default_factory=list) total: int = 0 offset: int = 0 limit: int = 50 # ── Search ─────────────────────────────────────────────────────────────────── class SearchResultItem(BaseModel): """A single search result.""" title: str slug: str = "" technique_page_slug: str = "" type: str = "" score: float = 0.0 summary: str = "" creator_name: str = "" creator_slug: str = "" topic_category: str = "" topic_tags: list[str] = Field(default_factory=list) match_context: str = "" class SearchResponse(BaseModel): """Top-level search response with metadata.""" items: list[SearchResultItem] = Field(default_factory=list) partial_matches: list[SearchResultItem] = Field(default_factory=list) total: int = 0 query: str = "" fallback_used: bool = False class SuggestionItem(BaseModel): """A single autocomplete suggestion.""" text: str type: Literal["topic", "technique", "creator"] class SuggestionsResponse(BaseModel): """Popular search suggestions for autocomplete.""" suggestions: list[SuggestionItem] = Field(default_factory=list) # ── Technique Page Detail ──────────────────────────────────────────────────── class KeyMomentSummary(BaseModel): """Lightweight key moment for technique page detail.""" model_config = ConfigDict(from_attributes=True) id: uuid.UUID title: str summary: str start_time: float end_time: float content_type: str plugins: list[str] | None = None source_video_id: uuid.UUID | None = None video_filename: str = "" class RelatedLinkItem(BaseModel): """A related technique link with target info.""" model_config = ConfigDict(from_attributes=True) target_title: str = "" target_slug: str = "" relationship: str = "" creator_name: str = "" topic_category: str = "" reason: str = "" class CreatorInfo(BaseModel): """Minimal creator info embedded in technique detail.""" model_config = ConfigDict(from_attributes=True) name: str slug: str genres: list[str] | None = None class SourceVideoSummary(BaseModel): """Lightweight source video info for technique page detail.""" model_config = ConfigDict(from_attributes=True) id: uuid.UUID filename: str content_type: str added_at: datetime | None = None class TechniquePageDetail(TechniquePageRead): """Technique page with nested key moments, creator, and related links.""" key_moments: list[KeyMomentSummary] = Field(default_factory=list) creator_info: CreatorInfo | None = None related_links: list[RelatedLinkItem] = Field(default_factory=list) version_count: int = 0 source_videos: list[SourceVideoSummary] = Field(default_factory=list) # ── Technique Page Versions ────────────────────────────────────────────────── class TechniquePageVersionSummary(BaseModel): """Lightweight version entry for list responses.""" model_config = ConfigDict(from_attributes=True) version_number: int created_at: datetime pipeline_metadata: dict | None = None class TechniquePageVersionDetail(BaseModel): """Full version snapshot for detail responses.""" model_config = ConfigDict(from_attributes=True) version_number: int content_snapshot: dict pipeline_metadata: dict | None = None created_at: datetime class TechniquePageVersionListResponse(BaseModel): """Response for version list endpoint.""" items: list[TechniquePageVersionSummary] = Field(default_factory=list) total: int = 0 # ── Topics ─────────────────────────────────────────────────────────────────── class TopicSubTopic(BaseModel): """A sub-topic with aggregated counts.""" name: str technique_count: int = 0 creator_count: int = 0 class TopicCategory(BaseModel): """A top-level topic category with sub-topics.""" name: str description: str = "" sub_topics: list[TopicSubTopic] = Field(default_factory=list) # ── Creator Browse ─────────────────────────────────────────────────────────── class CreatorBrowseItem(CreatorRead): """Creator with technique and video counts for browse pages.""" technique_count: int = 0 video_count: int = 0 # ── Content Reports ────────────────────────────────────────────────────────── class ContentReportCreate(BaseModel): """Public submission: report a content issue.""" content_type: str = Field( ..., description="Entity type: technique_page, key_moment, creator, general" ) content_id: uuid.UUID | None = Field( None, description="ID of the reported entity (null for general reports)" ) content_title: str | None = Field( None, description="Title of the reported content (for display context)" ) report_type: str = Field( ..., description="inaccurate, missing_info, wrong_attribution, formatting, other" ) description: str = Field( ..., min_length=10, max_length=2000, description="Description of the issue" ) page_url: str | None = Field( None, description="URL the user was on when reporting" ) class ContentReportRead(BaseModel): """Full report for admin views.""" model_config = ConfigDict(from_attributes=True) id: uuid.UUID content_type: str content_id: uuid.UUID | None = None content_title: str | None = None report_type: str description: str status: str = "open" admin_notes: str | None = None page_url: str | None = None created_at: datetime resolved_at: datetime | None = None class ContentReportUpdate(BaseModel): """Admin update: change status and/or add notes.""" status: str | None = Field( None, description="open, acknowledged, resolved, dismissed" ) admin_notes: str | None = Field( None, max_length=2000, description="Admin notes about resolution" ) class ContentReportListResponse(BaseModel): """Paginated list of content reports.""" items: list[ContentReportRead] = Field(default_factory=list) total: int = 0 offset: int = 0 limit: int = 50 # ── Pipeline Debug Mode ───────────────────────────────────────────────────── class DebugModeResponse(BaseModel): """Current debug mode status.""" debug_mode: bool class DebugModeUpdate(BaseModel): """Toggle debug mode on/off.""" debug_mode: bool class TokenStageSummary(BaseModel): """Per-stage token usage aggregation.""" stage: str call_count: int total_prompt_tokens: int total_completion_tokens: int total_tokens: int class TokenSummaryResponse(BaseModel): """Token usage summary for a video, broken down by stage.""" video_id: str stages: list[TokenStageSummary] = Field(default_factory=list) grand_total_tokens: int