import uuid from datetime import datetime, timezone from enum import Enum from typing import TYPE_CHECKING, Optional from pydantic import EmailStr from sqlalchemy import DateTime from sqlalchemy.orm import Mapped from sqlmodel import Field, Relationship, SQLModel if TYPE_CHECKING: pass def get_datetime_utc() -> datetime: return datetime.now(timezone.utc) # Shared properties class UserBase(SQLModel): email: EmailStr = Field(unique=True, index=True, max_length=255) is_active: bool = True is_superuser: bool = False full_name: str | None = Field(default=None, max_length=255) # Properties to receive via API on creation class UserCreate(UserBase): password: str = Field(min_length=8, max_length=128) class UserRegister(SQLModel): email: EmailStr = Field(max_length=255) password: str = Field(min_length=8, max_length=128) full_name: str | None = Field(default=None, max_length=255) # Properties to receive via API on update, all are optional class UserUpdate(UserBase): email: EmailStr | None = Field(default=None, max_length=255) # type: ignore password: str | None = Field(default=None, min_length=8, max_length=128) class UserUpdateMe(SQLModel): full_name: str | None = Field(default=None, max_length=255) email: EmailStr | None = Field(default=None, max_length=255) class UpdatePassword(SQLModel): current_password: str = Field(min_length=8, max_length=128) new_password: str = Field(min_length=8, max_length=128) # Database model, database table inferred from class name class User(UserBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) hashed_password: str created_at: datetime | None = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) items: Mapped[list["Item"]] = Relationship(back_populates="owner", cascade_delete=True) projects: Mapped[list["Project"]] = Relationship(back_populates="owner", cascade_delete=True) # Properties to return via API, id is always required class UserPublic(UserBase): id: uuid.UUID created_at: datetime | None = None class UsersPublic(SQLModel): data: list[UserPublic] count: int # Shared properties class ItemBase(SQLModel): title: str = Field(min_length=1, max_length=255) description: str | None = Field(default=None, max_length=255) # Properties to receive on item creation class ItemCreate(ItemBase): pass # Properties to receive on item update class ItemUpdate(ItemBase): title: str | None = Field(default=None, min_length=1, max_length=255) # type: ignore # Database model, database table inferred from class name class Item(ItemBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) created_at: datetime | None = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) owner_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) owner: Mapped[Optional["User"]] = Relationship(back_populates="items") # Properties to return via API, id is always required class ItemPublic(ItemBase): id: uuid.UUID owner_id: uuid.UUID created_at: datetime | None = None class ItemsPublic(SQLModel): data: list[ItemPublic] count: int # Project models for video clipper demo class ProjectStatus(str, Enum): UPLOADING = "uploading" ANALYZING = "analyzing" ANALYSIS_COMPLETE = "analysis_complete" PROCESSING = "processing" COMPLETED = "completed" FAILED = "failed" class ProjectSourceType(str, Enum): LOCAL_UPLOAD = "local_upload" YOUTUBE_URL = "youtube_url" # kept for DB compatibility; no longer creatable via UI/API GOOGLE_DRIVE_URL = "google_drive_url" ARTIST_SOCIAL = "artist_social" class TranscodingStatus(str, Enum): PENDING = "pending" PROCESSING = "processing" COMPLETE = "complete" FAILED = "failed" NOT_NEEDED = "not_needed" # Shared properties class ProjectBase(SQLModel): title: str = Field(max_length=255) source_type: ProjectSourceType source_url: str | None = Field(default=None, max_length=2048) num_clips: int = Field(default=3) min_duration: float | None = Field(default=None) max_duration: float | None = Field(default=None) crop_mode: str = Field(default="scene_equilibrium", max_length=50) detection_model: str = Field(default="mediapipe", max_length=50) custom_prompt: str | None = Field(default=None, max_length=2048) # Properties to receive via API on creation class ProjectCreate(ProjectBase): social_video_id: uuid.UUID | None = None # Properties to receive via API on update class ProjectUpdate(SQLModel): title: str | None = Field(default=None, max_length=255) num_clips: int | None = None min_duration: float | None = None max_duration: float | None = None crop_mode: str | None = Field(default=None, max_length=50) detection_model: str | None = Field(default=None, max_length=50) custom_prompt: str | None = Field(default=None, max_length=2048) # Caption settings caption_text: str | None = Field(default=None, max_length=512) caption_style: str | None = Field(default=None, max_length=30) caption_position: str | None = Field(default=None, max_length=10) # Cut-to-music settings cut_to_music_video_source: str | None = Field(default=None, max_length=20) chorus_start: float | None = None chorus_end: float | None = None chorus_description: str | None = Field(default=None, max_length=1024) # Individual clips video source setting clips_video_source: str | None = Field(default=None, max_length=20) # Database model class Project(ProjectBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) updated_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) # Ownership owner_id: uuid.UUID = Field( foreign_key="user.id", nullable=False, ondelete="CASCADE" ) owner: Mapped[Optional["User"]] = Relationship(back_populates="projects") # Video file video_filename: str = Field(max_length=255) video_path: str = Field(max_length=1024) # Video metadata (populated after upload) duration_seconds: float | None = None width: int | None = None height: int | None = None fps: float | None = None file_size_bytes: int | None = None # Status status: ProjectStatus = Field(default=ProjectStatus.UPLOADING) error_message: str | None = Field(default=None, max_length=2048) # Transcoding fields transcoded_path: str | None = Field(default=None, max_length=1024) transcoding_status: TranscodingStatus = Field(default=TranscodingStatus.PENDING) transcoding_error: str | None = Field(default=None, max_length=2048) # Caption overlay settings caption_text: str | None = Field(default=None, max_length=512) caption_style: str = Field(default="tiktok", max_length=30) caption_position: str = Field(default="center", max_length=10) # Cut-to-music settings cut_to_music_video_source: str = Field(default="all", max_length=20) chorus_start: float | None = None chorus_end: float | None = None chorus_description: str | None = Field(default=None, max_length=1024) # Individual clips video source setting clips_video_source: str = Field(default="main", max_length=20) # Relationships segments: Mapped[list["Segment"]] = Relationship( back_populates="project", cascade_delete=True ) clips: Mapped[list["Clip"]] = Relationship(back_populates="project", cascade_delete=True) extra_videos: Mapped[list["ExtraVideo"]] = Relationship( back_populates="project", cascade_delete=True ) cut_to_music_videos: Mapped[list["CutToMusicVideo"]] = Relationship( back_populates="project", cascade_delete=True ) # Properties to return via API class ProjectPublic(ProjectBase): id: uuid.UUID created_at: datetime updated_at: datetime owner_id: uuid.UUID owner_full_name: str | None = None video_filename: str video_path: str duration_seconds: float | None width: int | None height: int | None fps: float | None file_size_bytes: int | None status: ProjectStatus error_message: str | None transcoded_path: str | None transcoding_status: TranscodingStatus transcoding_error: str | None # Caption settings caption_text: str | None caption_style: str caption_position: str # Cut-to-music settings cut_to_music_video_source: str chorus_start: float | None chorus_end: float | None chorus_description: str | None # Individual clips video source clips_video_source: str class ProjectsPublic(SQLModel): data: list[ProjectPublic] count: int # Segment models # Shared properties class SegmentBase(SQLModel): start_time: float end_time: float score: float description: str = Field(max_length=1024) is_selected: bool = Field(default=True) order_index: int # Properties to receive via API on update class SegmentUpdate(SQLModel): start_time: float | None = None end_time: float | None = None is_selected: bool | None = None order_index: int | None = None caption_text_override: str | None = Field(default=None, max_length=512) caption_style_override: str | None = Field(default=None, max_length=30) caption_position_override: str | None = Field(default=None, max_length=10) # Database model class Segment(SegmentBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) project_id: uuid.UUID = Field( foreign_key="project.id", nullable=False, ondelete="CASCADE" ) project: Mapped[Optional["Project"]] = Relationship(back_populates="segments") # User modifications is_modified: bool = Field(default=False) # Per-segment caption overrides (None = inherit from project) caption_text_override: str | None = Field(default=None, max_length=512) caption_style_override: str | None = Field(default=None, max_length=30) caption_position_override: str | None = Field(default=None, max_length=10) # Source tracking for UGC segments (None = main project video) source_extra_video_id: uuid.UUID | None = Field( default=None, foreign_key="extravideo.id", nullable=True, ondelete="SET NULL", ) clip: Mapped[Optional["Clip"]] = Relationship(back_populates="segment") # Properties to return via API class SegmentPublic(SegmentBase): id: uuid.UUID created_at: datetime project_id: uuid.UUID is_modified: bool caption_text_override: str | None caption_style_override: str | None caption_position_override: str | None source_extra_video_id: uuid.UUID | None class SegmentsPublic(SQLModel): data: list[SegmentPublic] count: int # Clip models class ClipStatus(str, Enum): QUEUED = "queued" PROCESSING = "processing" COMPLETED = "completed" FAILED = "failed" # Shared properties class ClipBase(SQLModel): filename: str = Field(max_length=255) file_path: str = Field(max_length=1024) status: ClipStatus = Field(default=ClipStatus.QUEUED) progress_percent: float = Field(default=0.0) # Database model class Clip(ClipBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) completed_at: datetime | None = None project_id: uuid.UUID = Field( foreign_key="project.id", nullable=False, ondelete="CASCADE" ) project: Mapped[Optional["Project"]] = Relationship(back_populates="clips") segment_id: uuid.UUID = Field( foreign_key="segment.id", nullable=False, ondelete="CASCADE" ) segment: Mapped[Optional["Segment"]] = Relationship(back_populates="clip") # File information file_size_bytes: int | None = None duration_seconds: float | None = None error_message: str | None = Field(default=None, max_length=2048) # Properties to return via API class ClipPublic(ClipBase): id: uuid.UUID created_at: datetime completed_at: datetime | None project_id: uuid.UUID segment_id: uuid.UUID file_size_bytes: int | None duration_seconds: float | None error_message: str | None # Segment metadata populated at query time segment_score: float | None = None segment_description: str | None = None segment_start_time: float | None = None segment_end_time: float | None = None class ClipsPublic(SQLModel): data: list[ClipPublic] count: int # Processing Job models class JobType(str, Enum): ANALYZE_VIDEO = "analyze_video" GENERATE_CLIPS = "generate_clips" # Shared properties class ProcessingJobBase(SQLModel): job_type: JobType status: str = Field(default="pending", max_length=50) progress_percent: float = Field(default=0.0) current_step: str | None = Field(default=None, max_length=255) # Database model class ProcessingJob(ProcessingJobBase, table=True): id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) started_at: datetime | None = None completed_at: datetime | None = None project_id: uuid.UUID = Field( foreign_key="project.id", nullable=False, ondelete="CASCADE" ) celery_task_id: str | None = Field( default=None, max_length=255, unique=True, index=True ) error_message: str | None = Field(default=None, max_length=2048) # Properties to return via API class ProcessingJobPublic(ProcessingJobBase): id: uuid.UUID created_at: datetime started_at: datetime | None completed_at: datetime | None project_id: uuid.UUID celery_task_id: str | None error_message: str | None # Extra video models (additional sources for cut-to-music) class ExtraVideo(SQLModel, table=True): __tablename__ = "extravideo" id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) project_id: uuid.UUID = Field( foreign_key="project.id", nullable=False, ondelete="CASCADE" ) source_type: ProjectSourceType source_url: str | None = Field(default=None, max_length=2048) social_video_id: uuid.UUID | None = Field( default=None, foreign_key="socialvideo.id", nullable=True, ondelete="SET NULL" ) video_filename: str = Field(max_length=255) video_path: str | None = Field(default=None, max_length=1024) status: str = Field(default="pending", max_length=50) # pending | ready | failed error_message: str | None = Field(default=None, max_length=2048) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) project: Mapped[Optional["Project"]] = Relationship(back_populates="extra_videos") class ExtraVideoPublic(SQLModel): id: uuid.UUID project_id: uuid.UUID source_type: ProjectSourceType source_url: str | None social_video_id: uuid.UUID | None video_filename: str status: str error_message: str | None created_at: datetime # Cut-to-music video models class CutToMusicVideo(SQLModel, table=True): __tablename__ = "cuttomusicvideo" id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) project_id: uuid.UUID = Field( foreign_key="project.id", nullable=False, ondelete="CASCADE" ) run_index: int # 1 or 2 — distinguishes the two parallel runs filename: str | None = Field(default=None, max_length=255) file_path: str | None = Field(default=None, max_length=1024) status: ClipStatus = Field(default=ClipStatus.QUEUED) progress_percent: float = Field(default=0.0) error_message: str | None = Field(default=None, max_length=2048) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) completed_at: datetime | None = None file_size_bytes: int | None = None duration_seconds: float | None = None project: Mapped[Optional["Project"]] = Relationship(back_populates="cut_to_music_videos") class CutToMusicVideoPublic(SQLModel): id: uuid.UUID project_id: uuid.UUID run_index: int filename: str | None file_path: str | None status: ClipStatus progress_percent: float error_message: str | None created_at: datetime completed_at: datetime | None file_size_bytes: int | None duration_seconds: float | None # Generic message class Message(SQLModel): message: str # JSON payload containing access token class Token(SQLModel): access_token: str token_type: str = "bearer" # Contents of JWT token class TokenPayload(SQLModel): sub: str | None = None class NewPassword(SQLModel): token: str new_password: str = Field(min_length=8, max_length=128) # Artist Socials models class QualificationStatus(str, Enum): PENDING = "PENDING" PROCESSING = "PROCESSING" QUALIFIED = "QUALIFIED" DISQUALIFIED = "DISQUALIFIED" class SocialVideo(SQLModel, table=True): __tablename__ = "socialvideo" id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True) artist_slug: str = Field(max_length=100, index=True) filename: str = Field(max_length=500) video_path: str = Field(max_length=1000) # Metadata from TikTok JSON video_id: str | None = Field(default=None, max_length=100) video_duration: int | None = None video_timestamp: datetime | None = Field( default=None, sa_type=DateTime(timezone=False) # type: ignore ) author_name: str | None = Field(default=None, max_length=255) author_username: str | None = Field(default=None, max_length=100) video_description: str | None = Field(default=None, max_length=2000) video_playcount: int | None = None # Qualification qualification_status: QualificationStatus = Field(default=QualificationStatus.PENDING) is_qualified: bool | None = None disqualification_reason: str | None = Field(default=None, max_length=500) analyzed_at: datetime | None = Field( default=None, sa_type=DateTime(timezone=True) # type: ignore ) created_at: datetime = Field( default_factory=get_datetime_utc, sa_type=DateTime(timezone=True), # type: ignore ) class SocialVideoPublic(SQLModel): id: uuid.UUID artist_slug: str filename: str video_path: str video_id: str | None video_duration: int | None video_timestamp: datetime | None author_name: str | None author_username: str | None video_description: str | None video_playcount: int | None qualification_status: QualificationStatus is_qualified: bool | None disqualification_reason: str | None analyzed_at: datetime | None created_at: datetime class SocialVideosPublic(SQLModel): data: list[SocialVideoPublic] count: int class ArtistPublic(SQLModel): slug: str name: str video_count: int