338 lines
10 KiB
Python
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
|