- "alembic/versions/012_multi_source_format.py" - "backend/models.py" - "backend/schemas.py" GSD-Task: S03/T01
434 lines
14 KiB
Python
434 lines
14 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 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
|