diff --git a/alembic/versions/025_add_generated_shorts.py b/alembic/versions/025_add_generated_shorts.py new file mode 100644 index 0000000..156af79 --- /dev/null +++ b/alembic/versions/025_add_generated_shorts.py @@ -0,0 +1,45 @@ +"""Add generated_shorts table with format_preset and short_status enums. + +Revision ID: 025_add_generated_shorts +Revises: 024_add_posts_and_attachments +""" + +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import UUID + +from alembic import op + +revision = "025_add_generated_shorts" +down_revision = "024_add_posts_and_attachments" +branch_labels = None +depends_on = None + +format_preset_enum = sa.Enum("vertical", "square", "horizontal", name="format_preset") +short_status_enum = sa.Enum("pending", "processing", "complete", "failed", name="short_status") + + +def upgrade() -> None: + format_preset_enum.create(op.get_bind(), checkfirst=True) + short_status_enum.create(op.get_bind(), checkfirst=True) + + op.create_table( + "generated_shorts", + sa.Column("id", UUID(as_uuid=True), primary_key=True, server_default=sa.func.gen_random_uuid()), + sa.Column("highlight_candidate_id", UUID(as_uuid=True), sa.ForeignKey("highlight_candidates.id", ondelete="CASCADE"), nullable=False, index=True), + sa.Column("format_preset", format_preset_enum, nullable=False), + sa.Column("minio_object_key", sa.String(1000), nullable=True), + sa.Column("duration_secs", sa.Float, nullable=True), + sa.Column("width", sa.Integer, nullable=False), + sa.Column("height", sa.Integer, nullable=False), + sa.Column("file_size_bytes", sa.BigInteger, nullable=True), + sa.Column("status", short_status_enum, nullable=False, server_default="pending"), + sa.Column("error_message", sa.Text, nullable=True), + sa.Column("created_at", sa.DateTime, nullable=False, server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime, nullable=False, server_default=sa.func.now()), + ) + + +def downgrade() -> None: + op.drop_table("generated_shorts") + short_status_enum.drop(op.get_bind(), checkfirst=True) + format_preset_enum.drop(op.get_bind(), checkfirst=True) diff --git a/backend/config.py b/backend/config.py index 11e963e..5336275 100644 --- a/backend/config.py +++ b/backend/config.py @@ -81,6 +81,7 @@ class Settings(BaseSettings): # File storage transcript_storage_path: str = "/data/transcripts" video_metadata_path: str = "/data/video_meta" + video_source_path: str = "/videos" # Git commit SHA (set at Docker build time or via env var) git_commit_sha: str = "unknown" diff --git a/backend/models.py b/backend/models.py index 94df94f..1d77731 100644 --- a/backend/models.py +++ b/backend/models.py @@ -814,3 +814,55 @@ class PostAttachment(Base): # relationships post: Mapped[Post] = sa_relationship(back_populates="attachments") + + +# ── Shorts Generation ──────────────────────────────────────────────────────── + +class FormatPreset(str, enum.Enum): + """Output format presets for generated shorts.""" + vertical = "vertical" # 9:16 (1080x1920) + square = "square" # 1:1 (1080x1080) + horizontal = "horizontal" # 16:9 (1920x1080) + + +class ShortStatus(str, enum.Enum): + """Processing status for a generated short.""" + pending = "pending" + processing = "processing" + complete = "complete" + failed = "failed" + + +class GeneratedShort(Base): + """A video short generated from a highlight candidate in a specific format.""" + __tablename__ = "generated_shorts" + + id: Mapped[uuid.UUID] = _uuid_pk() + highlight_candidate_id: Mapped[uuid.UUID] = mapped_column( + ForeignKey("highlight_candidates.id", ondelete="CASCADE"), + nullable=False, index=True, + ) + format_preset: Mapped[FormatPreset] = mapped_column( + Enum(FormatPreset, name="format_preset", create_constraint=True), + nullable=False, + ) + minio_object_key: Mapped[str | None] = mapped_column(String(1000), nullable=True) + duration_secs: Mapped[float | None] = mapped_column(Float, nullable=True) + width: Mapped[int] = mapped_column(Integer, nullable=False) + height: Mapped[int] = mapped_column(Integer, nullable=False) + file_size_bytes: Mapped[int | None] = mapped_column(BigInteger, nullable=True) + status: Mapped[ShortStatus] = mapped_column( + Enum(ShortStatus, name="short_status", create_constraint=True), + default=ShortStatus.pending, + server_default="pending", + ) + error_message: Mapped[str | None] = mapped_column(Text, nullable=True) + created_at: Mapped[datetime] = mapped_column( + default=_now, server_default=func.now() + ) + updated_at: Mapped[datetime] = mapped_column( + default=_now, server_default=func.now(), onupdate=_now + ) + + # relationships + highlight_candidate: Mapped[HighlightCandidate] = sa_relationship() diff --git a/docker-compose.yml b/docker-compose.yml index a4e8327..5d16c98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -125,6 +125,7 @@ services: volumes: - /vmPool/r/services/chrysopedia_data:/data - ./config:/config:ro + - /vmPool/r/services/chrysopedia_videos:/videos:ro depends_on: chrysopedia-db: condition: service_healthy @@ -165,6 +166,7 @@ services: - /vmPool/r/services/chrysopedia_data:/data - ./prompts:/prompts:ro - ./config:/config:ro + - /vmPool/r/services/chrysopedia_videos:/videos:ro depends_on: chrysopedia-db: condition: service_healthy diff --git a/docker/Dockerfile.api b/docker/Dockerfile.api index ba9a122..7dd27ea 100644 --- a/docker/Dockerfile.api +++ b/docker/Dockerfile.api @@ -4,7 +4,7 @@ WORKDIR /app # System deps RUN apt-get update && apt-get install -y --no-install-recommends \ - gcc libpq-dev curl \ + gcc libpq-dev curl ffmpeg \ && rm -rf /var/lib/apt/lists/* # Python deps (cached layer)