eeb8a05b69
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m4s
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m31s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 4m11s
AI Code Review / AI Code Review (pull_request) Successful in 8m48s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m46s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 24m20s
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
- viral_video_jobs: 爆款视频任务主表(含 v1.3 reference_video/style_strength/style_guide 字段) - viral_video_style_templates: 风格模板配置(seed 4 个系统模板) - viral_video_prompt_templates: Prompt 模板(由 #2040 seed,7 种 prompt_type) - 单元测试 4 个,覆盖三表 CRUD 与默认值
989 lines
46 KiB
Python
Executable File
989 lines
46 KiB
Python
Executable File
from datetime import UTC, datetime
|
||
from typing import Any
|
||
|
||
from sqlalchemy import (
|
||
JSON,
|
||
Boolean,
|
||
Column,
|
||
DateTime,
|
||
Float,
|
||
ForeignKey,
|
||
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)
|
||
member_type = Column(String(20), nullable=True)
|
||
member_expires_at = Column(DateTime, nullable=True)
|
||
points_balance = Column(Integer, nullable=False, default=0)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC), index=True)
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(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(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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)
|
||
# #1970 原子化切片:片段选中的原子片段 ID(空串表示旧的整条素材选取路径)
|
||
atom_clip_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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(
|
||
DateTime,
|
||
nullable=False,
|
||
default=lambda: datetime.now(UTC),
|
||
onupdate=lambda: datetime.now(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(UTC))
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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(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(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(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(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(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(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(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(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(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)
|
||
style = Column(String(32), nullable=False, default="")
|
||
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(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(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)
|
||
# #2001 封面独立标题配置(结构同 title_config;为空时封面不叠标题)
|
||
cover_title_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(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class PointsAccountModel(Base):
|
||
"""积分账户 ORM 模型 (#1895)"""
|
||
|
||
__tablename__ = "points_accounts"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, unique=True, index=True)
|
||
balance = Column(Integer, nullable=False, default=0)
|
||
total_earned = Column(Integer, nullable=False, default=0)
|
||
total_spent = Column(Integer, nullable=False, default=0)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class PointsTransactionModel(Base):
|
||
"""积分流水 ORM 模型 (#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, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class PointsOrderModel(Base):
|
||
"""积分/会员订单 ORM 模型 (#1895)"""
|
||
|
||
__tablename__ = "points_orders"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
order_type = Column(String(20), nullable=False) # membership / points
|
||
product_code = Column(String(50), nullable=False)
|
||
amount_cents = Column(Integer, nullable=False)
|
||
original_amount_cents = Column(Integer, nullable=False, default=0)
|
||
discount = Column(Float, nullable=False, default=1.0)
|
||
points_amount = Column(Integer, nullable=False, default=0)
|
||
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, nullable=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class AssetAtomClipModel(Base):
|
||
"""素材原子片段 ORM 模型 (#1970 智能剪辑流程重构)。
|
||
|
||
逻辑切分单元,不物理切割视频文件。
|
||
"""
|
||
|
||
__tablename__ = "asset_atom_clips"
|
||
__table_args__ = (UniqueConstraint("asset_id", "clip_index", name="uq_asset_atom_clips_asset_index"),)
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
asset_id = Column(
|
||
String(36),
|
||
ForeignKey("assets.id", ondelete="CASCADE"),
|
||
nullable=False,
|
||
index=True,
|
||
)
|
||
start_time = Column(Float, nullable=False)
|
||
end_time = Column(Float, nullable=False)
|
||
duration = Column(Float, nullable=False)
|
||
clip_index = Column(Integer, nullable=False)
|
||
tags = Column(JSON, nullable=False, default=list)
|
||
ai_tags = Column(JSON, nullable=True, default=None)
|
||
caption = Column(Text, nullable=True, default=None)
|
||
embedding = Column(JSON, nullable=True, default=None)
|
||
scene_change_at = Column(Float, nullable=True)
|
||
is_fallback = Column(Boolean, nullable=False, default=False)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class DailyUsageRecordModel(Base):
|
||
"""每日使用记录 ORM 模型 (#1895)"""
|
||
|
||
__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(DateTime, nullable=False) # stored as DATE in SQL but DateTime for ORM compat
|
||
usage_type = Column(String(50), nullable=False, default="free_clip")
|
||
count = Column(Integer, nullable=False, default=0)
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class GpuLipsyncTaskModel(Base):
|
||
"""GPU 口型同步任务 ORM 模型 — MuseTalk 反向轮询模式.
|
||
|
||
业务侧(AI 数字人生成/lipsync 流程)提交任务后,GPU Worker 主动 poll 拉取、
|
||
调用本地 MuseTalk 推理、再通过 result 接口回传结果视频。
|
||
"""
|
||
|
||
__tablename__ = "gpu_lipsync_tasks"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
# 业务关联(原 lipsync_job_id,方便双向查询)
|
||
lipsync_job_id = Column(String(36), nullable=False, default="", index=True)
|
||
user_id = Column(String(36), nullable=False, default="", index=True)
|
||
project_id = Column(String(36), nullable=False, default="", index=True)
|
||
|
||
# 输入(预签名下载 URL,由 API 侧生成)
|
||
video_url = Column(Text, nullable=False)
|
||
audio_url = Column(Text, nullable=False)
|
||
|
||
# 结果
|
||
result_url = Column(Text, nullable=False, default="")
|
||
result_duration = Column(Float, nullable=False, default=0.0)
|
||
|
||
# 任务状态
|
||
status = Column(
|
||
String(20),
|
||
nullable=False,
|
||
default="pending",
|
||
index=True,
|
||
) # pending → processing → done / failed / timeout
|
||
worker_id = Column(String(100), nullable=False, default="", index=True)
|
||
attempt = Column(Integer, nullable=False, default=0)
|
||
error_msg = Column(Text, nullable=False, default="")
|
||
|
||
# 时间戳
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
started_at = Column(DateTime, nullable=True)
|
||
finished_at = Column(DateTime, nullable=True)
|
||
updated_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
# 心跳:worker 最近一次 poll/result 的时间,用于判定 worker 失联
|
||
last_heartbeat_at = Column(DateTime, nullable=True)
|
||
|
||
|
||
class GpuWorkerModel(Base):
|
||
"""GPU Worker 注册表 — 反向轮询模式下用于心跳与监控."""
|
||
|
||
__tablename__ = "gpu_workers"
|
||
|
||
worker_id = Column(String(100), primary_key=True)
|
||
hostname = Column(String(200), nullable=False, default="")
|
||
gpu_name = Column(String(200), nullable=False, default="")
|
||
free_vram_mb = Column(Integer, nullable=False, default=0)
|
||
capabilities = Column(String(500), nullable=False, default="") # 逗号分隔,如 "musetalk"
|
||
last_heartbeat_at = Column(DateTime, nullable=True, index=True)
|
||
created_at = Column(DateTime, nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class ViralVideoJobModel(Base):
|
||
"""爆款视频任务"""
|
||
|
||
__tablename__ = "viral_video_jobs"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
user_id = Column(String(36), nullable=False, index=True)
|
||
images = Column(JSON, nullable=False, default=list) # 产品图片 URL 列表
|
||
industry = Column(String(100), nullable=False, default="")
|
||
target_customer = Column(String(500), nullable=False, default="")
|
||
persona_id = Column(String(36), nullable=False, default="")
|
||
viral_structure = Column(String(50), nullable=False, default="")
|
||
marketing_purpose = Column(String(100), nullable=False, default="")
|
||
bgm_preference = Column(String(50), nullable=False, default="")
|
||
duration = Column(Integer, nullable=False, default=30)
|
||
user_copy_text = Column(Text, nullable=False, default="")
|
||
fusion_level = Column(String(20), nullable=False, default="ai_polish")
|
||
reference_audio_path = Column(String(1000), nullable=False, default="")
|
||
# v1.3 新增字段
|
||
reference_video_url = Column(String(1000), nullable=False, default="")
|
||
style_strength = Column(String(20), nullable=False, default="medium")
|
||
style_guide = Column(JSON, nullable=True)
|
||
style_template_id = Column(String(36), nullable=False, default="", index=True)
|
||
# 结果与状态
|
||
status = Column(String(30), nullable=False, default="pending", index=True)
|
||
intent_result = Column(JSON, nullable=True)
|
||
result_video_url = Column(String(1000), nullable=False, default="")
|
||
credits_cost = Column(Integer, nullable=False, default=0)
|
||
error_msg = Column(Text, nullable=False, default="")
|
||
retry_count = Column(Integer, nullable=False, default=0)
|
||
started_at = Column(DateTime(timezone=True), nullable=True)
|
||
completed_at = Column(DateTime(timezone=True), nullable=True)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class ViralVideoStyleTemplateModel(Base):
|
||
"""爆款视频风格模板配置表"""
|
||
|
||
__tablename__ = "viral_video_style_templates"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
name = Column(String(200), nullable=False)
|
||
description = Column(Text, nullable=False, default="")
|
||
thumbnail_url = Column(String(1000), nullable=False, default="")
|
||
style_config = Column(JSON, nullable=False, default=dict)
|
||
is_system = Column(Boolean, nullable=False, default=True, index=True)
|
||
sort_order = Column(Integer, nullable=False, default=0)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
|
||
|
||
class ViralVideoPromptTemplateModel(Base):
|
||
"""爆款视频 Prompt 模板表(由 #2040 seed)"""
|
||
|
||
__tablename__ = "viral_video_prompt_templates"
|
||
|
||
id = Column(String(36), primary_key=True)
|
||
prompt_type = Column(String(50), nullable=False, index=True)
|
||
name = Column(String(200), nullable=False)
|
||
content = Column(Text, nullable=False, default="")
|
||
variables = Column(JSON, nullable=False, default=list)
|
||
version = Column(Integer, nullable=False, default=1)
|
||
is_active = Column(Boolean, nullable=False, default=True, index=True)
|
||
created_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|
||
updated_at = Column(DateTime(timezone=True), nullable=False, default=lambda: datetime.now(UTC))
|