from datetime import datetime from typing import TYPE_CHECKING, List, Optional from sqlalchemy import DateTime, Float, ForeignKey, Integer, String, Text, func from sqlalchemy.orm import Mapped, mapped_column, relationship from app.database.session import Base if TYPE_CHECKING: from app.home.models import Project from app.lyric.models import Lyric from app.video.models import Video class Song(Base): """ 노래 테이블 AI를 통해 생성된 노래 정보를 저장합니다. 가사를 기반으로 생성됩니다. Attributes: id: 고유 식별자 (자동 증가) project_id: 연결된 Project의 id (외래키) lyric_id: 연결된 Lyric의 id (외래키) task_id: 노래 생성 작업의 고유 식별자 (UUID 형식) suno_task_id: Suno API 작업 고유 식별자 (선택) status: 처리 상태 (processing, uploading, completed, failed) song_prompt: 노래 생성에 사용된 프롬프트 song_result_url: 생성 결과 URL (선택) language: 출력 언어 created_at: 생성 일시 (자동 설정) Relationships: project: 연결된 Project lyric: 연결된 Lyric videos: 이 노래를 사용한 영상 결과 목록 """ __tablename__ = "song" __table_args__ = ( { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci", }, ) id: Mapped[int] = mapped_column( Integer, primary_key=True, nullable=False, autoincrement=True, comment="고유 식별자", ) project_id: Mapped[int] = mapped_column( Integer, ForeignKey("project.id", ondelete="CASCADE"), nullable=False, index=True, comment="연결된 Project의 id", ) lyric_id: Mapped[int] = mapped_column( Integer, ForeignKey("lyric.id", ondelete="CASCADE"), nullable=False, index=True, comment="연결된 Lyric의 id", ) task_id: Mapped[str] = mapped_column( String(36), nullable=False, comment="노래 생성 작업 고유 식별자 (UUID)", ) suno_task_id: Mapped[Optional[str]] = mapped_column( String(64), nullable=True, comment="Suno API 작업 고유 식별자", ) suno_audio_id: Mapped[Optional[str]] = mapped_column( String(64), nullable=True, comment="Suno 첫번째 노래의 고유 식별자", ) status: Mapped[str] = mapped_column( String(50), nullable=False, comment="처리 상태 (processing, uploading, completed, failed)", ) song_prompt: Mapped[str] = mapped_column( Text, nullable=False, comment="노래 생성에 사용된 프롬프트", ) song_result_url: Mapped[Optional[str]] = mapped_column( String(2048), nullable=True, comment="노래 결과 URL", ) duration: Mapped[Optional[float]] = mapped_column( nullable=True, comment="노래 재생 시간 (초)", ) language: Mapped[str] = mapped_column( String(50), nullable=False, default="Korean", comment="출력 언어 (Korean, English, Chinese, Japanese, Thai, Vietnamese)", ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now(), comment="생성 일시", ) # Relationships project: Mapped["Project"] = relationship( "Project", back_populates="songs", ) lyric: Mapped["Lyric"] = relationship( "Lyric", back_populates="songs", ) videos: Mapped[List["Video"]] = relationship( "Video", back_populates="song", cascade="all, delete-orphan", lazy="selectin", ) def __repr__(self) -> str: def truncate(value: str | None, max_len: int = 10) -> str: if value is None: return "None" return (value[:max_len] + "...") if len(value) > max_len else value return ( f"" ) class SongTimestamp(Base): """ 노래 타임스탬프 테이블 노래의 가사별 시작/종료 시간 정보를 저장합니다. Suno API에서 반환된 타임스탬프 데이터를 기반으로 생성됩니다. Attributes: id: 고유 식별자 (자동 증가) suno_audio_id: 가사의 원본 오디오 ID order_idx: 오디오 내에서 가사의 순서 lyric_line: 가사 한 줄의 내용 start_time: 가사 시작 시점 (초) end_time: 가사 종료 시점 (초) created_at: 생성 일시 (자동 설정) """ __tablename__ = "song_timestamp" __table_args__ = ( { "mysql_engine": "InnoDB", "mysql_charset": "utf8mb4", "mysql_collate": "utf8mb4_unicode_ci", }, ) id: Mapped[int] = mapped_column( Integer, primary_key=True, nullable=False, autoincrement=True, comment="고유 식별자", ) suno_audio_id: Mapped[str] = mapped_column( String(64), nullable=False, index=True, comment="가사의 원본 오디오 ID", ) order_idx: Mapped[int] = mapped_column( Integer, nullable=False, comment="오디오 내에서 가사의 순서", ) lyric_line: Mapped[str] = mapped_column( Text, nullable=False, comment="가사 한 줄의 내용", ) start_time: Mapped[float] = mapped_column( Float, nullable=False, comment="가사 시작 시점 (초)", ) end_time: Mapped[float] = mapped_column( Float, nullable=False, comment="가사 종료 시점 (초)", ) created_at: Mapped[datetime] = mapped_column( DateTime, nullable=False, server_default=func.now(), comment="생성 일시", ) def __repr__(self) -> str: def truncate(value: str | None, max_len: int = 10) -> str: if value is None: return "None" return (value[:max_len] + "...") if len(value) > max_len else value return ( f"" )