834 lines
40 KiB
Python
834 lines
40 KiB
Python
from datetime import datetime, timezone
|
||
from typing import Any
|
||
|
||
from sqlalchemy import (
|
||
JSON,
|
||
Boolean,
|
||
Column,
|
||
Date,
|
||
DateTime,
|
||
Float,
|
||
Index,
|
||
Integer,
|
||
String,
|
||
Text,
|
||
UniqueConstraint,
|
||
text,
|
||
)
|
||
from sqlalchemy.orm import declarative_base
|
||
|
||
Base: Any = declarative_base()
|
||
|
||
|
||
class UserModel(Base):
|
||
__tablename__ = "users"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
email = Column(String(255), nullable=False, unique=True, index=True)
|
||
username = Column(String(100), nullable=True, unique=True, index=True)
|
||
display_name = Column(String(255), nullable=False)
|
||
password_hash = Column(String(255), nullable=False)
|
||
email_verified = Column(Boolean, nullable=False, default=False)
|
||
email_verification_token = Column(String(255), nullable=True)
|
||
password_reset_token = Column(String(255), nullable=True)
|
||
password_reset_expires_at = Column(DateTime, nullable=True)
|
||
last_login_at = Column(DateTime, nullable=True)
|
||
last_login_ip = Column(String(50), nullable=True)
|
||
# 订阅相关字段 (移到 User 级别)
|
||
subscription_plan = Column(String(20), nullable=False, default="free")
|
||
subscription_status = Column(String(20), nullable=False, default="active")
|
||
subscription_expires_at = Column(DateTime, nullable=True)
|
||
# 配额限制 (移到 User 级别)
|
||
max_projects = Column(Integer, nullable=False, default=3)
|
||
max_storage_gb = Column(Integer, nullable=False, default=10)
|
||
used_storage_gb = Column(Integer, nullable=False, default=0)
|
||
# 管理员标识
|
||
is_admin = Column(Boolean, nullable=False, default=False)
|
||
# 微信登录
|
||
wechat_openid = Column(String(128), nullable=True, unique=True, index=True)
|
||
wechat_unionid = Column(String(128), nullable=True, unique=True, index=True)
|
||
# 手机号绑定
|
||
phone = Column(String(32), nullable=True, unique=True, index=True)
|
||
phone_verified = Column(Boolean, nullable=False, default=False)
|
||
binding_completed_at = Column(DateTime, nullable=True)
|
||
profile_completed = Column(Boolean, nullable=False, default=True, server_default="true")
|
||
# 会员 + 积分(#1895)
|
||
is_member = Column(Boolean, nullable=False, default=False, server_default="false")
|
||
member_type = Column(String(20), nullable=True) # monthly / quarterly / yearly
|
||
member_expires_at = Column(DateTime, nullable=True)
|
||
points_balance = Column(Integer, nullable=False, default=0, server_default="0")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class ProjectModel(Base):
|
||
__tablename__ = "projects"
|
||
__table_args__ = (
|
||
# Issue #1775: 每个用户至多一个默认项目(部分唯一索引,只约束 is_default=true 的行)。
|
||
# 注意:不加 UniqueConstraint(那会要求全表唯一),用部分索引表达"每用户一个默认项目"。
|
||
Index("uq_projects_owner_default", "owner_user_id", unique=True, postgresql_where=text("is_default = true")),
|
||
)
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
owner_user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(100), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
shared_users = Column(JSON, nullable=False, default=list) # 被共享的用户 ID 列表
|
||
is_default = Column(Boolean, nullable=False, default=False, server_default="false")
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class AssetLibraryModel(Base):
|
||
__tablename__ = "asset_libraries"
|
||
__table_args__ = (UniqueConstraint("project_id", "kind", name="uq_asset_libraries_project_kind"),)
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=True, index=True)
|
||
name = Column(String(255), nullable=False)
|
||
kind = Column(String(20), nullable=False, index=True)
|
||
asset_count = Column(Float, nullable=False, default=0)
|
||
total_size = Column(Float, nullable=False, default=0)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class AssetModel(Base):
|
||
__tablename__ = "assets"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
asset_library_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(500), nullable=False)
|
||
file_type = Column(String(20), nullable=False, index=True)
|
||
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
|
||
file_size = Column(Integer, nullable=False)
|
||
# file_url: 完整可访问的 URL,用于客户端直接访问文件
|
||
file_url = Column(String(1000), nullable=False)
|
||
# storage_key: OSS 存储键,用于内部下载上传
|
||
storage_key = Column(String(500), nullable=False, default="", server_default="")
|
||
thumbnail_url = Column(String(1000), nullable=True)
|
||
duration = Column(Float, nullable=True)
|
||
width = Column(Float, nullable=True)
|
||
height = Column(Float, nullable=True)
|
||
fps = Column(Float, nullable=True)
|
||
codec = Column(String(50), nullable=True)
|
||
status = Column(String(20), nullable=False, default="uploading", index=True)
|
||
classification_status = Column(String(20), nullable=False, default="pending", index=True)
|
||
classification_result = Column(Text, nullable=True)
|
||
quality_score = Column(Float, nullable=True)
|
||
uploaded_by_user_id = Column(String(36), nullable=False)
|
||
file_hash = Column(String(64), nullable=True, index=True)
|
||
client_upload_id = Column(String(64), nullable=True, index=True)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class TagModel(Base):
|
||
"""标签 ORM 模型。"""
|
||
|
||
__tablename__ = "tags"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(100), nullable=False)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
__table_args__ = (UniqueConstraint("user_id", "name", name="uq_tags_user_name"),)
|
||
|
||
|
||
class AssetTagModel(Base):
|
||
"""素材-标签关联表 ORM 模型。"""
|
||
|
||
__tablename__ = "asset_tags"
|
||
|
||
asset_id = Column(String(36), primary_key=True)
|
||
tag_id = Column(String(36), primary_key=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class EditTemplateModel(Base):
|
||
"""Phase 8 剪辑模板 ORM 模型
|
||
|
||
全局模板库中的模板,定义剪辑风格、配置参数和预览信息。
|
||
"""
|
||
|
||
__tablename__ = "edit_templates"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
name = Column(String(120), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
template_type = Column(String(50), nullable=False, default="default", index=True)
|
||
editing_mode = Column(String(20), nullable=False, default="one_take")
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
preview_url = Column(String(1000), nullable=False, default="")
|
||
sort_weight = Column(Integer, nullable=False, default=0, index=True)
|
||
status = Column(String(20), nullable=False, default="active", index=True)
|
||
version = Column(Integer, nullable=False, default=1)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class EditTemplateVersionModel(Base):
|
||
"""模板发布版本快照 ORM 模型
|
||
|
||
每次发布保存完整快照,支持版本历史查询和回滚。
|
||
"""
|
||
|
||
__tablename__ = "edit_template_versions"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
template_id = Column(String(32), nullable=False, index=True)
|
||
version = Column(Integer, nullable=False)
|
||
name = Column(String(200), nullable=False, default="")
|
||
editing_mode = Column(String(30), nullable=False, default="one_take")
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
clip_configs = Column(JSON, nullable=False, default=list)
|
||
change_note = Column(String(500), nullable=False, default="")
|
||
published_by = Column(String(36), nullable=False, default="")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class EditPlanModel(Base):
|
||
"""Phase 8 剪辑计划 ORM 模型
|
||
|
||
基于某个 EditTemplate 创建的剪辑计划,包含具体的配置和状态追踪。
|
||
"""
|
||
|
||
__tablename__ = "edit_plans"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
template_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(200), nullable=False)
|
||
status = Column(String(20), nullable=False, default="draft", index=True)
|
||
total_duration = Column(Float, nullable=False, default=0.0)
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
source_edit_plan_id = Column(String(36), nullable=True, index=True)
|
||
project_id = Column(String(36), nullable=False, default="", index=True)
|
||
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class TemplateClipConfigModel(Base):
|
||
"""Phase 8 模板片段配置 ORM 模型
|
||
|
||
定义模板中每个片段的规则:类型、时长范围、文案模板、素材要求、转场效果。
|
||
"""
|
||
|
||
__tablename__ = "template_clip_configs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
template_id = Column(String(36), nullable=False, index=True)
|
||
clip_type = Column(String(20), nullable=False, index=True)
|
||
order = Column(Integer, nullable=False)
|
||
min_duration = Column(Float, nullable=False, default=0.0)
|
||
max_duration = Column(Float, nullable=False, default=0.0)
|
||
text_template = Column(Text, nullable=False, default="")
|
||
material_requirements = Column(JSON, nullable=False, default=dict)
|
||
transition_effect = Column(String(20), nullable=False, default="cut")
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class EditPlanClipModel(Base):
|
||
"""Phase 8 剪辑计划片段 ORM 模型
|
||
|
||
剪辑计划中的具体片段实例,关联 EditPlan 和可选的 TemplateClipConfig。
|
||
"""
|
||
|
||
__tablename__ = "edit_plan_clips"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
plan_id = Column(String(36), nullable=False, index=True)
|
||
clip_type = Column(String(20), nullable=False, index=True)
|
||
order = Column(Integer, nullable=False)
|
||
template_clip_config_id = Column(String(36), nullable=False, default="", index=True)
|
||
asset_id = Column(String(36), nullable=False, default="", index=True)
|
||
text_content = Column(Text, nullable=False, default="")
|
||
start_time = Column(Float, nullable=False, default=0.0)
|
||
duration = Column(Float, nullable=False, default=0.0)
|
||
transition_effect = Column(String(20), nullable=False, default="cut")
|
||
transition_duration = Column(Float, nullable=False, default=0.0)
|
||
playback_speed = Column(Float, nullable=False, default=1.0)
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class IngestJobModel(Base):
|
||
__tablename__ = "ingest_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
library_id = Column(String(36), nullable=False, index=True)
|
||
storage_key = Column(String(255), nullable=False)
|
||
status = Column(String(20), nullable=False, default="pending")
|
||
error_message = Column(Text, nullable=False, default="")
|
||
result_asset_id = Column(String(36), nullable=False, default="")
|
||
file_hash = Column(String(64), nullable=True, index=True)
|
||
asset_id = Column(String(36), nullable=False, default="", index=True)
|
||
celery_task_id = Column(String(64), nullable=False, default="", server_default="")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class ClassificationJobModel(Base):
|
||
__tablename__ = "classification_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
asset_id = Column(String(36), nullable=False, index=True)
|
||
status = Column(String(20), nullable=False, default="pending")
|
||
classification = Column(String(50), nullable=False, default="")
|
||
confidence = Column(Float, nullable=False, default=0.0)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class GenerationTaskModel(Base):
|
||
__tablename__ = "generation_tasks"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, default="", index=True)
|
||
strategy_id = Column(String(36), nullable=False, default="")
|
||
asset_library_id = Column(String(36), nullable=False, default="", index=True)
|
||
voice_library_id = Column(String(36), nullable=False, default="")
|
||
template_id = Column(String(36), nullable=False, default="", index=True)
|
||
asset_ids = Column(JSON, nullable=False, default=list)
|
||
title_ids = Column(JSON, nullable=False, default=list)
|
||
voice_ids = Column(JSON, nullable=False, default=list)
|
||
editing_mode = Column(
|
||
String(20), nullable=False, default="one_take", index=True
|
||
) # 剪辑模式: one_take, pip, voice_over, voice_pip
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
progress = Column(Float, nullable=False, default=0.0)
|
||
result_count = Column(Float, nullable=False, default=0)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
error_info = Column(JSON, nullable=True)
|
||
retry_count = Column(Integer, nullable=False, default=0)
|
||
auto_retry_enabled = Column(Boolean, nullable=False, default=False)
|
||
auto_retry_max = Column(Integer, nullable=False, default=0)
|
||
started_at = Column(DateTime, nullable=True)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
|
||
source_edit_plan_id = Column(String(36), nullable=True, index=True)
|
||
edit_plan_id = Column(String(36), nullable=True, index=True)
|
||
asset_select_mode = Column(String(20), nullable=False, default="")
|
||
batch_id = Column(String(36), nullable=False, default="", index=True)
|
||
video_title = Column(String(255), nullable=False, default="")
|
||
resolution = Column(String(20), nullable=False, default="")
|
||
is_preview = Column(Boolean, nullable=False, default=False, index=True)
|
||
source_task_id = Column(String(32), nullable=False, default="", index=True)
|
||
celery_task_id = Column(String(64), nullable=False, default="", server_default="")
|
||
output_width = Column(Integer, nullable=False, default=1280)
|
||
output_height = Column(Integer, nullable=False, default=720)
|
||
cover_url = Column(String(1000), nullable=False, default="")
|
||
custom_title = Column(String(500), nullable=False, default="")
|
||
title_config = Column(JSON, nullable=False, default=dict)
|
||
bgm_config = Column(JSON, nullable=False, default=dict)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
logs = Column(Text, nullable=False, default="[]", server_default="[]")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(
|
||
DateTime,
|
||
nullable=False,
|
||
default=lambda: datetime.now(timezone.utc),
|
||
onupdate=lambda: datetime.now(timezone.utc),
|
||
)
|
||
|
||
|
||
class GeneratedVideoModel(Base):
|
||
__tablename__ = "generated_videos"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
user_id = Column(String(36), nullable=False, default="", index=True)
|
||
generation_task_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(255), nullable=False)
|
||
# file_url: 完整可访问的 URL,用于客户端直接访问视频
|
||
file_url = Column(String(1000), nullable=False)
|
||
# file_size: 文件大小(字节),使用 Integer 类型以确保精确性
|
||
file_size = Column(Integer, nullable=False)
|
||
duration = Column(Float, nullable=False)
|
||
thumbnail_url = Column(String(1000), nullable=True)
|
||
width = Column(Float, nullable=False)
|
||
height = Column(Float, nullable=False)
|
||
fps = Column(Float, nullable=False)
|
||
status = Column(String(20), nullable=False, default="completed", index=True)
|
||
review_status = Column(String(20), nullable=False, default="pending_review", index=True)
|
||
generation_params = Column(Text, nullable=False, default="{}")
|
||
generated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=True)
|
||
video_fingerprint = Column(Text, nullable=True)
|
||
is_duplicate = Column(Boolean, nullable=False, default=False)
|
||
duplicate_of = Column(String(36), nullable=True)
|
||
duplicate_rate = Column(Float, nullable=True)
|
||
match_count = Column(Integer, nullable=True, default=0)
|
||
visual_similarity = Column(Float, nullable=True, default=0.0)
|
||
|
||
|
||
class TitleLibraryModel(Base):
|
||
__tablename__ = "title_libraries"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(255), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
category = Column(String(50), nullable=False, default="default", index=True)
|
||
text = Column(String(500), nullable=False)
|
||
tags = Column(JSON, nullable=False, default=list)
|
||
usage_count = Column(Integer, nullable=False, default=0)
|
||
is_active = Column(Boolean, nullable=False, default=True, index=True)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class VoiceCloneProfileModel(Base):
|
||
"""音色克隆档案 ORM 模型。"""
|
||
|
||
__tablename__ = "voice_clone_profiles"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(100), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
source_audio_url = Column(Text, nullable=False, default="")
|
||
voice_id = Column(String(100), nullable=False, default="")
|
||
voice_model = Column(String(100), nullable=False, default="")
|
||
language = Column(String(20), nullable=False, default="zh-CN")
|
||
gender = Column(String(20), nullable=False, default="unknown")
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
retry_count = Column(Integer, nullable=False, default=0)
|
||
max_retries = Column(Integer, nullable=False, default=3)
|
||
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class VoiceLibraryModel(Base):
|
||
__tablename__ = "voice_libraries"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
project_id = Column(String(36), nullable=True, index=True)
|
||
name = Column(String(255), nullable=False)
|
||
text = Column(Text, nullable=False, default="")
|
||
voice_provider = Column(String(50), nullable=False, default="")
|
||
voice_id = Column(String(100), nullable=False, default="")
|
||
voice_name = Column(String(100), nullable=False, default="")
|
||
audio_url = Column(String(1000), nullable=False, default="")
|
||
duration = Column(Float, nullable=False, default=0)
|
||
file_size = Column(Integer, nullable=False, default=0)
|
||
status = Column(String(20), nullable=False, default="completed", index=True)
|
||
tags = Column(JSON, nullable=False, default=list)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class DuplicationRecordModel(Base):
|
||
__tablename__ = "duplication_records"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
filename = Column(String(500), nullable=False)
|
||
file_size = Column(Integer, nullable=False)
|
||
storage_key = Column(String(500), nullable=False)
|
||
duration_seconds = Column(Float, nullable=False, default=0)
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
duplicate_rate = Column(Float, nullable=True)
|
||
duplicate_count = Column(Integer, nullable=False, default=0)
|
||
# #1661 手动查重:视觉相似度(0~1)/ 匹配视频数
|
||
visual_similarity = Column(Float, nullable=True)
|
||
match_count = Column(Integer, nullable=True)
|
||
video_fingerprint = Column(Text, nullable=True)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class DuplicationSegmentModel(Base):
|
||
__tablename__ = "duplication_segments"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
record_id = Column(String(36), nullable=False, index=True)
|
||
source_start = Column(Float, nullable=False)
|
||
source_end = Column(Float, nullable=False)
|
||
matched_video_id = Column(String(36), nullable=False)
|
||
matched_video_name = Column(String(500), nullable=False, default="")
|
||
matched_start = Column(Float, nullable=False)
|
||
matched_end = Column(Float, nullable=False)
|
||
similarity = Column(Float, nullable=False)
|
||
|
||
|
||
class RecipeModel(Base):
|
||
__tablename__ = "recipes"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(200), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
template_id = Column(String(36), nullable=False, default="")
|
||
generation_params = Column(JSON, nullable=False, default=dict)
|
||
is_active = Column(Boolean, nullable=False, default=True)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class RecipeItemModel(Base):
|
||
__tablename__ = "recipe_items"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
recipe_id = Column(String(36), nullable=False, index=True)
|
||
item_type = Column(String(20), nullable=False)
|
||
item_id = Column(String(36), nullable=False)
|
||
position = Column(Integer, nullable=False, default=0)
|
||
extra_meta = Column("metadata", JSON, nullable=False, default=dict)
|
||
|
||
|
||
class TemplateModel(Base):
|
||
__tablename__ = "templates"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(200), nullable=False)
|
||
mode = Column(String(30), nullable=False, index=True) # EditingMode 枚举值: pip / voice_pip / one_take / voice_over
|
||
category = Column(String(100), nullable=False, default="")
|
||
tags = Column(JSON, nullable=False, default=list)
|
||
title_config = Column(JSON, nullable=False, default=dict)
|
||
subtitle_config = Column(JSON, nullable=False, default=dict)
|
||
bgm_config = Column(JSON, nullable=False, default=dict)
|
||
estimated_duration = Column(Float, nullable=False, default=0.0)
|
||
is_active = Column(Boolean, nullable=False, default=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class TemplateSegmentModel(Base):
|
||
__tablename__ = "template_segments"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
template_id = Column(String(36), nullable=False, index=True)
|
||
segment_order = Column(Integer, nullable=False)
|
||
duration_min = Column(Float, nullable=False)
|
||
duration_max = Column(Float, nullable=False)
|
||
material_type = Column(String(20), nullable=True) # 仅 voice_over 模式: 人物/场景
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class TemplateCategoryModel(Base):
|
||
__tablename__ = "template_categories"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
name = Column(String(100), nullable=False)
|
||
sort_order = Column(Integer, nullable=False, default=0)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class JobModel(Base):
|
||
"""Phase 8 任务 2.10 — 统一异步任务 ORM 模型。"""
|
||
|
||
__tablename__ = "jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
job_type = Column(String(30), nullable=False, index=True)
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
progress = Column(Float, nullable=False, default=0.0)
|
||
current_stage = Column(String(200), nullable=False, default="")
|
||
payload = Column(JSON, nullable=False, default=dict)
|
||
result = Column(JSON, nullable=False, default=dict)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
retry_count = Column(Integer, nullable=False, default=0)
|
||
max_retries = Column(Integer, nullable=False, default=3)
|
||
celery_task_id = Column(String(100), nullable=False, default="")
|
||
source_id = Column(String(36), nullable=False, default="", index=True)
|
||
created_by_user_id = Column(String(36), nullable=False, default="", index=True)
|
||
started_at = Column(DateTime, nullable=True)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class TTSJobModel(Base):
|
||
"""TTS 合成任务 ORM 模型。"""
|
||
|
||
__tablename__ = "tts_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
input_text = Column(Text, nullable=False)
|
||
voice_id = Column(String(100), nullable=False, default="")
|
||
voice_model = Column(String(100), nullable=False, default="")
|
||
project_id = Column(String(36), nullable=False, default="")
|
||
voice_clone_profile_id = Column(String(36), nullable=False, default="")
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
output_audio_url = Column(Text, nullable=False, default="")
|
||
output_audio_key = Column(String(500), nullable=False, default="")
|
||
duration = Column(Float, nullable=False, default=0.0)
|
||
file_size = Column(Integer, nullable=False, default=0)
|
||
sample_rate = Column(Integer, nullable=False, default=22050)
|
||
format = Column(String(20), nullable=False, default="mp3")
|
||
error_message = Column(Text, nullable=False, default="")
|
||
retry_count = Column(Integer, nullable=False, default=0)
|
||
max_retries = Column(Integer, nullable=False, default=3)
|
||
metadata_ = Column("metadata", JSON, nullable=False, default=dict)
|
||
started_at = Column(DateTime, nullable=True)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class BillingRecordModel(Base):
|
||
"""账单记录"""
|
||
|
||
__tablename__ = "billing_records"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
plan_name = Column(String(50), nullable=False)
|
||
amount = Column(Float, nullable=False)
|
||
billing_cycle = Column(String(20), nullable=False)
|
||
status = Column(String(20), nullable=False, default="pending") # pending, paid, failed, refunded
|
||
payment_method = Column(String(50), nullable=True)
|
||
payment_id = Column(String(100), nullable=True) # 第三方支付流水号
|
||
invoice_url = Column(String(500), nullable=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
paid_at = Column(DateTime, nullable=True)
|
||
|
||
|
||
class VerificationCodeModel(Base):
|
||
"""验证码(邮箱/手机统一管理)"""
|
||
|
||
__tablename__ = "verification_codes"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
recipient = Column(String(255), nullable=False, index=True) # 邮箱或手机号
|
||
code = Column(String(10), nullable=False)
|
||
code_type = Column(String(32), nullable=False, index=True) # email_bind / phone_bind / ...
|
||
expires_at = Column(DateTime, nullable=False)
|
||
used_at = Column(DateTime, nullable=True)
|
||
attempts = Column(Integer, nullable=False, default=0)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class VideoShareModel(Base):
|
||
"""视频分享记录."""
|
||
|
||
__tablename__ = "video_shares"
|
||
|
||
id = Column(String(32), primary_key=True)
|
||
video_id = Column(String(32), nullable=False, index=True)
|
||
user_id = Column(String(32), nullable=False, index=True)
|
||
share_token = Column(String(16), nullable=False, unique=True)
|
||
password_hash = Column(String(255), nullable=True)
|
||
expires_at = Column(DateTime(timezone=True), nullable=True)
|
||
view_count = Column(Integer, nullable=False, default=0)
|
||
download_count = Column(Integer, nullable=False, default=0)
|
||
is_active = Column(Boolean, nullable=False, default=True)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class CoverTemplateModel(Base):
|
||
"""封面模板"""
|
||
|
||
__tablename__ = "cover_templates"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=True, index=True) # NULL = 系统模板
|
||
name = Column(String(200), nullable=False)
|
||
thumbnail_url = Column(String(1000), nullable=False, default="")
|
||
is_system = Column(Boolean, nullable=False, default=False, index=True)
|
||
config = Column(JSON, nullable=False, default=dict)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class VideoFingerprintChunkModel(Base):
|
||
"""分片视频指纹 — 每个视频按时间分片存储 pHash + color_histogram."""
|
||
|
||
__tablename__ = "video_fingerprint_chunks"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
video_id = Column(String(36), nullable=False, index=True)
|
||
project_id = Column(String(36), nullable=False, index=True)
|
||
user_id = Column(String(36), nullable=False, index=True, default="")
|
||
start_time_ms = Column(Integer, nullable=False)
|
||
end_time_ms = Column(Integer, nullable=False)
|
||
phash_binary = Column(String(16), nullable=False)
|
||
color_histogram = Column(JSON, nullable=False)
|
||
frame_count = Column(Integer, nullable=False, default=1)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class ScriptModel(Base):
|
||
"""口播文案库 (Issue #1795)"""
|
||
|
||
__tablename__ = "scripts"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
title = Column(String(255), nullable=False)
|
||
content = Column(Text, nullable=False, default="")
|
||
segments = Column(JSON, nullable=False, default=list)
|
||
tags = Column(JSON, nullable=False, default=list)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class LipsyncJobModel(Base):
|
||
"""对口型任务 ORM 模型 — #1796 MediaKit 对口型.
|
||
|
||
记录用户提交的对口型任务,跟踪 MediaKit 异步任务状态。
|
||
"""
|
||
|
||
__tablename__ = "lipsync_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
project_id = Column(String(36), nullable=False, default="", index=True)
|
||
|
||
# 输入参数
|
||
video_url = Column(Text, nullable=False)
|
||
audio_url = Column(Text, nullable=True) # 直生模式(voice_id+script_text)下 TTS 合成后回填
|
||
enable_video_loop = Column(Boolean, nullable=False, default=False)
|
||
|
||
# TTS 直生字段:传音色 + 文案,由后端先合成音频再对口型
|
||
voice_id = Column(String(200), nullable=False, default="")
|
||
script_text = Column(Text, nullable=False, default="")
|
||
speed = Column(Float, nullable=False, default=1.0)
|
||
emotion = Column(String(20), nullable=False, default="")
|
||
|
||
# MediaKit 任务状态
|
||
mediakit_task_id = Column(String(200), nullable=False, default="", index=True)
|
||
status = Column(
|
||
String(20), nullable=False, default="pending", index=True
|
||
) # pending → submitted → processing → completed → failed
|
||
output_video_url = Column(Text, nullable=False, default="")
|
||
output_duration = Column(Float, nullable=False, default=0.0)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
error_code = Column(String(100), nullable=False, default="")
|
||
|
||
# 精确句子时间戳(TTS 合成后由 silencedetect 计算,用于 B-roll 精确定位)
|
||
sentence_timings = Column(JSON, nullable=True) # list[{index,text,start_time,end_time}]
|
||
|
||
# 时间戳
|
||
submitted_at = Column(DateTime, nullable=True)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class AiAvatarRenderJob(Base):
|
||
"""AI数字人渲染任务 — #1798"""
|
||
|
||
__tablename__ = "ai_avatar_render_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
project_id = Column(String(36), nullable=False, default="", index=True)
|
||
|
||
# 输入参数
|
||
lipsync_job_id = Column(String(36), nullable=False)
|
||
# 文案 ID 可选:手动输入文案(TTS 直生)场景不关联文案库条目
|
||
script_id = Column(String(36), nullable=False, default="")
|
||
b_roll_segments = Column(JSON, nullable=False, default=list)
|
||
# b_roll_segments 格式: [{"script_segment_index": 0, "asset_url": "...", "mode": "fullscreen|pip", "start_time": 5.0, "end_time": 10.0}, ...]
|
||
title_config = Column(JSON, nullable=False, default=dict)
|
||
cover_config = Column(JSON, nullable=False, default=dict)
|
||
|
||
# 任务状态
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
progress = Column(Integer, nullable=False, default=0)
|
||
output_video_url = Column(Text, nullable=False, default="")
|
||
output_cover_url = Column(Text, nullable=False, default="")
|
||
output_duration = Column(Float, nullable=False, default=0.0)
|
||
error_message = Column(Text, nullable=False, default="")
|
||
submitted_at = Column(DateTime, nullable=True)
|
||
started_at = Column(DateTime, nullable=True)
|
||
completed_at = Column(DateTime, nullable=True)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class PointsAccountModel(Base):
|
||
"""积分账户(#1895) — 每用户一条记录,记录余额及累计值."""
|
||
|
||
__tablename__ = "points_accounts"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(
|
||
String(36), nullable=False, unique=True, index=True
|
||
) # UNIQUE FK handled by ForeignKeyConstraint in migration
|
||
balance = Column(Integer, nullable=False, default=0, server_default="0")
|
||
total_earned = Column(Integer, nullable=False, default=0, server_default="0")
|
||
total_spent = Column(Integer, nullable=False, default=0, server_default="0")
|
||
total_purchased = Column(Integer, nullable=False, default=0, server_default="0")
|
||
total_gifted = Column(Integer, nullable=False, default=0, server_default="0")
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class PointsTransactionModel(Base):
|
||
"""积分流水(#1895) — 每笔积分变动一条记录,只追加不修改."""
|
||
|
||
__tablename__ = "points_transactions"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
account_id = Column(String(36), nullable=False, index=True)
|
||
type = Column(String(20), nullable=False, index=True) # earn / spend / refund
|
||
source = Column(String(50), nullable=False, index=True)
|
||
amount = Column(Integer, nullable=False)
|
||
balance_after = Column(Integer, nullable=False)
|
||
description = Column(String(255), nullable=False, default="")
|
||
ref_id = Column(String(100), nullable=False, default="")
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc), index=True)
|
||
|
||
|
||
class PointsOrderModel(Base):
|
||
"""积分充值订单(#1895)."""
|
||
|
||
__tablename__ = "points_orders"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
package_name = Column(String(50), nullable=False)
|
||
points_amount = Column(Integer, nullable=False)
|
||
price_cents = Column(Integer, nullable=False)
|
||
currency = Column(String(10), nullable=False, default="CNY")
|
||
discount = Column(Float, nullable=False, default=1.0)
|
||
original_price_cents = Column(Integer, nullable=False)
|
||
status = Column(String(20), nullable=False, default="pending", index=True)
|
||
payment_method = Column(String(50), nullable=True)
|
||
payment_id = Column(String(100), nullable=True)
|
||
paid_at = Column(DateTime(timezone=True), nullable=True)
|
||
expire_at = Column(DateTime(timezone=True), nullable=True)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|
||
|
||
|
||
class DailyUsageRecordModel(Base):
|
||
"""每日使用计数(#1895) — DB 持久化兜底,Redis 实时计数."""
|
||
|
||
__tablename__ = "daily_usage_records"
|
||
__table_args__ = (UniqueConstraint("user_id", "usage_date", "usage_type", name="uq_daily_usage_user_date_type"),)
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
usage_date = Column(Date, nullable=False)
|
||
usage_type = Column(String(50), nullable=False)
|
||
count = Column(Integer, nullable=False, default=0, server_default="0")
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(timezone.utc))
|