chrysopedia/backend/schemas.py
jlightner 0c4162a777 feat: Added video_filename field to KeyMomentSummary schema and populat…
- "backend/schemas.py"
- "backend/routers/techniques.py"

GSD-Task: S03/T01
2026-03-30 06:50:01 +00:00

338 lines
10 KiB
Python

"""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
# ── Search ───────────────────────────────────────────────────────────────────
class SearchResultItem(BaseModel):
"""A single search result."""
title: str
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)
class SearchResponse(BaseModel):
"""Top-level search response with metadata."""
items: list[SearchResultItem] = Field(default_factory=list)
total: int = 0
query: str = ""
fallback_used: bool = False
# ── 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
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 = ""
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 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)
# ── 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