acd23's picture
Upload src/models/database.py
dd59a8c verified
"""
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()