| """ |
| Database Models -- AI Reel Creator Platform |
| =========================================== |
| |
| SQLAlchemy async ORM models mapping exactly to configs/database_schema.sql. |
| """ |
|
|
| import uuid |
| from datetime import datetime |
| from typing import List, Optional |
|
|
| from sqlalchemy import ( |
| create_engine, Column, String, Text, Integer, BigInteger, Float, |
| Boolean, DateTime, ForeignKey, ARRAY, JSON, func, event, select, |
| ) |
| from sqlalchemy.orm import declarative_base, relationship, Session |
| from sqlalchemy.dialects.postgresql import UUID, JSONB, ENUM |
|
|
| _TRY_PGVECTOR = True |
| Vector = None |
|
|
| try: |
| from pgvector.sqlalchemy import Vector as _Vector |
| Vector = _Vector |
| except ImportError: |
| _TRY_PGVECTOR = False |
| class _VectorStub: |
| def __init__(self, dim: int): |
| self.dim = dim |
| Vector = _VectorStub |
|
|
| Base = declarative_base() |
|
|
|
|
| class TimestampMixin: |
| created_at = Column(DateTime(timezone=True), server_default=func.now()) |
| updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) |
|
|
|
|
| class Asset(Base, TimestampMixin): |
| __tablename__ = "assets" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| file_path = Column(Text, nullable=False) |
| file_name = Column(Text, nullable=False) |
| asset_type = Column(ENUM("video", "image", "audio", name="asset_type_enum"), nullable=False) |
| source = Column(Text) |
| resolution = Column(Text) |
| duration_ms = Column(Integer) |
| frame_rate = Column(Float) |
| file_size_bytes = Column(BigInteger) |
| metadata = relationship("AssetMetadata", uselist=False, back_populates="asset") |
| video_events = relationship("VideoEvent", back_populates="asset") |
|
|
|
|
| class AssetMetadata(Base, TimestampMixin): |
| __tablename__ = "asset_metadata" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| asset_id = Column(UUID(as_uuid=True), ForeignKey("assets.id", ondelete="CASCADE"), nullable=False, unique=True) |
| description = Column(Text) |
| shot_type = Column(ENUM("close_up", "medium_shot", "wide_shot", "extreme_close_up", "establishing_shot", "aerial", "tracking", "static", "pan", "tilt", "drone", "360", name="shot_type_enum")) |
| camera_angle = Column(ENUM("front", "rear", "side_left", "side_right", "top_down", "low_angle", "eye_level", "three_quarter", "interior", "detail", name="camera_angle_enum")) |
| subject_part = Column(Text) |
| mood = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="mood_enum")) |
| dominant_colours = Column(ARRAY(Text)) |
| confidence_score = Column(Float) |
| review_flag = Column(Boolean, default=False) |
| embedding_768 = Column(Vector(768) if _TRY_PGVECTOR else JSONB) |
| embedding_model = Column(Text, default="openai/clip-vit-large-patch14") |
| extracted_at = Column(DateTime(timezone=True), server_default=func.now()) |
| asset = relationship("Asset", back_populates="metadata") |
|
|
|
|
| class VideoEvent(Base, TimestampMixin): |
| __tablename__ = "video_events" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| asset_id = Column(UUID(as_uuid=True), ForeignKey("assets.id", ondelete="CASCADE"), nullable=False) |
| start_ms = Column(Integer, nullable=False) |
| end_ms = Column(Integer, nullable=False) |
| duration_ms = Column(Integer) |
| description = Column(Text) |
| shot_type = Column(ENUM("close_up", "medium_shot", "wide_shot", "extreme_close_up", "establishing_shot", "aerial", "tracking", "static", "pan", "tilt", "drone", "360", name="shot_type_enum")) |
| camera_angle = Column(ENUM("front", "rear", "side_left", "side_right", "top_down", "low_angle", "eye_level", "three_quarter", "interior", "detail", name="camera_angle_enum")) |
| subject_part = Column(Text) |
| mood = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="mood_enum")) |
| embedding_768 = Column(Vector(768) if _TRY_PGVECTOR else JSONB) |
| confidence_score = Column(Float) |
| keyframe_path = Column(Text) |
| asset = relationship("Asset", back_populates="video_events") |
|
|
|
|
| class BrandConfig(Base, TimestampMixin): |
| __tablename__ = "brand_configs" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| name = Column(Text, nullable=False) |
| version = Column(Integer, nullable=False, default=1) |
| colours = Column(JSONB, nullable=False, default=dict) |
| typography = Column(JSONB, nullable=False, default=dict) |
| tone_of_voice = Column(JSONB, nullable=False, default=dict) |
| approved_terminology = Column(ARRAY(Text)) |
| restricted_visuals = Column(ARRAY(Text)) |
| logo_paths = Column(JSONB) |
|
|
|
|
| class BrochureNode(Base, TimestampMixin): |
| __tablename__ = "brochure_nodes" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| section = Column(ENUM("Exterior", "Interior", "Performance", "Safety", "Technology", "Comfort", "Sustainability", "Brand_Story", name="section_enum"), nullable=False) |
| title = Column(Text, nullable=False) |
| content = Column(Text, nullable=False) |
| key_features = Column(ARRAY(Text)) |
| taglines = Column(ARRAY(Text)) |
| spec_highlights = Column(JSONB) |
| car_part_referenced = Column(ARRAY(Text)) |
| tone_tags = Column(ARRAY(Text)) |
| embedding_768 = Column(Vector(768) if _TRY_PGVECTOR else JSONB) |
| embedding_model = Column(Text, default="openai/clip-vit-large-patch14") |
| page_number = Column(Integer) |
| source_pdf = Column(Text) |
| mappings = relationship("BrochureAssetMap", back_populates="brochure_node") |
| captions = relationship("CaptionLibrary", back_populates="brochure_node") |
| voiceovers = relationship("VoiceoverLibrary", back_populates="brochure_node") |
|
|
|
|
| class BrochureAssetMap(Base, TimestampMixin): |
| __tablename__ = "brochure_asset_map" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| brochure_node_id = Column(UUID(as_uuid=True), ForeignKey("brochure_nodes.id", ondelete="CASCADE"), nullable=False) |
| asset_id = Column(UUID(as_uuid=True), ForeignKey("assets.id", ondelete="CASCADE"), nullable=False) |
| video_event_id = Column(UUID(as_uuid=True), ForeignKey("video_events.id", ondelete="SET NULL")) |
| similarity_score = Column(Float, nullable=False) |
| mapping_type = Column(ENUM("semantic", "rule_based", "manual", "override", name="mapping_type_enum"), default="semantic") |
| confidence_score = Column(Float, nullable=False) |
| is_approved = Column(Boolean, default=None) |
| reviewer_notes = Column(Text) |
| rank = Column(Integer, nullable=False) |
| brochure_node = relationship("BrochureNode", back_populates="mappings") |
|
|
|
|
| class CaptionLibrary(Base, TimestampMixin): |
| __tablename__ = "captions_library" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| brochure_node_id = Column(UUID(as_uuid=True), ForeignKey("brochure_nodes.id", ondelete="SET NULL")) |
| car_part = Column(Text) |
| tone = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="tone_enum"), nullable=False) |
| duration_class = Column(ENUM("short", "medium", "long", name="duration_class_enum"), nullable=False, default="medium") |
| text = Column(Text, nullable=False) |
| word_count = Column(Integer) |
| is_brand_compliant = Column(Boolean, default=True) |
| compliance_notes = Column(Text) |
| usage_count = Column(Integer, default=0) |
| brochure_node = relationship("BrochureNode", back_populates="captions") |
|
|
|
|
| class VoiceoverLibrary(Base, TimestampMixin): |
| __tablename__ = "voiceover_library" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| brochure_node_id = Column(UUID(as_uuid=True), ForeignKey("brochure_nodes.id", ondelete="SET NULL")) |
| car_part = Column(Text) |
| tone = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="tone_enum"), nullable=False) |
| duration_class = Column(ENUM("short", "medium", "long", name="duration_class_enum"), nullable=False, default="medium") |
| text = Column(Text, nullable=False) |
| estimated_duration_ms = Column(Integer) |
| is_brand_compliant = Column(Boolean, default=True) |
| compliance_notes = Column(Text) |
| usage_count = Column(Integer, default=0) |
| brochure_node = relationship("BrochureNode", back_populates="voiceovers") |
|
|
|
|
| class ReelRequest(Base, TimestampMixin): |
| __tablename__ = "reel_requests" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| user_query = Column(Text, nullable=False) |
| duration_target = Column(ENUM("10s", "20s", "30s", "60s", "custom", name="duration_target_enum"), nullable=False) |
| duration_ms = Column(Integer) |
| platform = Column(ENUM("instagram_reels", "tiktok", "youtube_shorts", "linkedin", "twitter", "facebook", "custom", name="platform_enum"), nullable=False) |
| tone = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="tone_enum"), nullable=False) |
| aspect_ratio = Column(ENUM("9:16", "16:9", "1:1", "4:5", name="aspect_ratio_enum"), nullable=False, default="9:16") |
| brand_config_id = Column(UUID(as_uuid=True), ForeignKey("brand_configs.id")) |
| additional_constraints = Column(JSONB) |
| status = Column(ENUM("pending", "planning", "generating", "review", "completed", "failed", name="reel_status_enum"), nullable=False, default="pending") |
| error_message = Column(Text) |
| script = relationship("ReelScript", uselist=False, back_populates="reel_request") |
| manifest = relationship("ReelManifest", uselist=False, back_populates="reel_request") |
|
|
|
|
| class ReelScript(Base, TimestampMixin): |
| __tablename__ = "reel_scripts" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| reel_request_id = Column(UUID(as_uuid=True), ForeignKey("reel_requests.id", ondelete="CASCADE"), nullable=False, unique=True) |
| script_json = Column(JSONB, nullable=False) |
| total_beats = Column(Integer, nullable=False) |
| total_duration_ms = Column(Integer, nullable=False) |
| validation_status = Column(ENUM("pending", "valid", "invalid", "corrected", name="validation_status_enum"), nullable=False, default="pending") |
| validation_errors = Column(ARRAY(Text)) |
| generation_attempts = Column(Integer, default=1) |
| reel_request = relationship("ReelRequest", back_populates="script") |
|
|
|
|
| class BeatAssetCandidate(Base, TimestampMixin): |
| __tablename__ = "beat_asset_candidates" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| reel_request_id = Column(UUID(as_uuid=True), ForeignKey("reel_requests.id", ondelete="CASCADE"), nullable=False) |
| beat_number = Column(Integer, nullable=False) |
| beat_intent = Column(Text, nullable=False) |
| car_parts = Column(ARRAY(Text)) |
| tone = Column(ENUM("sporty", "elegant", "technical", "luxury", "adventure", "minimal", "dynamic", "serene", name="tone_enum")) |
| asset_id = Column(UUID(as_uuid=True), ForeignKey("assets.id", ondelete="SET NULL")) |
| video_event_id = Column(UUID(as_uuid=True), ForeignKey("video_events.id", ondelete="SET NULL")) |
| similarity_score = Column(Float) |
| rank = Column(Integer, nullable=False) |
| mapping_source = Column(ENUM("semantic", "brochure_map", "rule_based", "fallback", name="mapping_source_enum"), default="semantic") |
| is_selected = Column(Boolean, default=False) |
|
|
|
|
| class ReelManifest(Base, TimestampMixin): |
| __tablename__ = "reel_manifests" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| reel_request_id = Column(UUID(as_uuid=True), ForeignKey("reel_requests.id", ondelete="CASCADE"), nullable=False, unique=True) |
| manifest_json = Column(JSONB, nullable=False) |
| total_duration_ms = Column(Integer, nullable=False) |
| beat_count = Column(Integer, nullable=False) |
| asset_count = Column(Integer, nullable=False) |
| is_validated = Column(Boolean, default=False) |
| validation_report = Column(JSONB) |
| render_status = Column(ENUM("pending", "rendering", "completed", "failed", name="render_status_enum"), nullable=False, default="pending") |
| render_output_path = Column(Text) |
| remotion_export_path = Column(Text) |
| reel_request = relationship("ReelRequest", back_populates="manifest") |
|
|
|
|
| class AuditLog(Base): |
| __tablename__ = "audit_log" |
| id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4) |
| table_name = Column(Text, nullable=False) |
| record_id = Column(UUID(as_uuid=True), nullable=False) |
| action = Column(ENUM("INSERT", "UPDATE", "DELETE", name="audit_action_enum"), nullable=False) |
| old_data = Column(JSONB) |
| new_data = Column(JSONB) |
| performed_by = Column(Text) |
| performed_at = Column(DateTime(timezone=True), server_default=func.now()) |
|
|
|
|
| def get_engine(database_url: Optional[str] = None): |
| url = database_url or os.environ.get("DATABASE_URL", "postgresql://reel_user:reel_password@localhost:5432/reel_creator") |
| return create_engine(url, future=True) |
|
|
|
|
| def init_db(engine=None): |
| engine = engine or get_engine() |
| Base.metadata.create_all(engine) |
| print("Database tables created / verified.") |
|
|
|
|
| if __name__ == "__main__": |
| init_db() |
|
|