From eeb8a05b690e0f5e2a6ec92da8c63fb5c6f1be3f Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 29 Sep 2026 19:28:12 +0800 Subject: [PATCH 1/3] feat(#2039): add viral video DB models + alembic migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 与默认值 --- .../versions/086_add_viral_video_tables.py | 102 ++++++++++++++++++ packages/adapters/sqlalchemy_impl/models.py | 68 ++++++++++++ tests/unit/test_viral_video_models.py | 99 +++++++++++++++++ 3 files changed, 269 insertions(+) create mode 100755 alembic/versions/086_add_viral_video_tables.py create mode 100644 tests/unit/test_viral_video_models.py diff --git a/alembic/versions/086_add_viral_video_tables.py b/alembic/versions/086_add_viral_video_tables.py new file mode 100755 index 000000000..99e09d374 --- /dev/null +++ b/alembic/versions/086_add_viral_video_tables.py @@ -0,0 +1,102 @@ +"""add viral video tables + +Revision ID: 086_add_viral_video_tables +Revises: 085_atom_clip_caption_embedding +Create Date: 2026-09-28 + +新增爆款视频相关表: +- viral_video_jobs: 爆款视频任务 +- viral_video_style_templates: 风格模板配置 +- viral_video_prompt_templates: Prompt 模板(由 #2040 seed) +""" + +import sqlalchemy as sa + +from alembic import op + +revision = "086_add_viral_video_tables" +down_revision = "085_atom_clip_caption_embedding" +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # viral_video_jobs + op.create_table( + "viral_video_jobs", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("user_id", sa.String(36), nullable=False, index=True), + sa.Column("images", sa.JSON(), nullable=False, server_default="[]"), + sa.Column("industry", sa.String(100), nullable=False, server_default=""), + sa.Column("target_customer", sa.String(500), nullable=False, server_default=""), + sa.Column("persona_id", sa.String(36), nullable=False, server_default=""), + sa.Column("viral_structure", sa.String(50), nullable=False, server_default=""), + sa.Column("marketing_purpose", sa.String(100), nullable=False, server_default=""), + sa.Column("bgm_preference", sa.String(50), nullable=False, server_default=""), + sa.Column("duration", sa.Integer(), nullable=False, server_default="30"), + sa.Column("user_copy_text", sa.Text(), nullable=False, server_default=""), + sa.Column("fusion_level", sa.String(20), nullable=False, server_default="ai_polish"), + sa.Column("reference_audio_path", sa.String(1000), nullable=False, server_default=""), + # v1.3 新增 + sa.Column("reference_video_url", sa.String(1000), nullable=False, server_default=""), + sa.Column("style_strength", sa.String(20), nullable=False, server_default="medium"), + sa.Column("style_guide", sa.JSON(), nullable=True), + sa.Column("style_template_id", sa.String(36), nullable=False, server_default="", index=True), + # 状态与结果 + sa.Column("status", sa.String(30), nullable=False, server_default="pending", index=True), + sa.Column("intent_result", sa.JSON(), nullable=True), + sa.Column("result_video_url", sa.String(1000), nullable=False, server_default=""), + sa.Column("credits_cost", sa.Integer(), nullable=False, server_default="0"), + sa.Column("error_msg", sa.Text(), nullable=False, server_default=""), + sa.Column("retry_count", sa.Integer(), nullable=False, server_default="0"), + sa.Column("started_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("completed_at", sa.DateTime(timezone=True), nullable=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + ) + + # viral_video_style_templates + op.create_table( + "viral_video_style_templates", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("name", sa.String(200), nullable=False), + sa.Column("description", sa.Text(), nullable=False, server_default=""), + sa.Column("thumbnail_url", sa.String(1000), nullable=False, server_default=""), + sa.Column("style_config", sa.JSON(), nullable=False, server_default="{}"), + sa.Column("is_system", sa.Boolean(), nullable=False, server_default=sa.text("true"), index=True), + sa.Column("sort_order", sa.Integer(), nullable=False, server_default="0"), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + ) + + # viral_video_prompt_templates + op.create_table( + "viral_video_prompt_templates", + sa.Column("id", sa.String(36), primary_key=True), + sa.Column("prompt_type", sa.String(50), nullable=False, index=True), + sa.Column("name", sa.String(200), nullable=False), + sa.Column("content", sa.Text(), nullable=False, server_default=""), + sa.Column("variables", sa.JSON(), nullable=False, server_default="[]"), + sa.Column("version", sa.Integer(), nullable=False, server_default="1"), + sa.Column("is_active", sa.Boolean(), nullable=False, server_default=sa.text("true"), index=True), + sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.func.now()), + ) + + # Seed 默认风格模板 + op.execute( + """ + INSERT INTO viral_video_style_templates (id, name, description, style_config, is_system, sort_order) + VALUES + ('style-tpl-001', '快节奏冲击', '高频切镜+动感BGM,适合食品饮料等快消品', '{"cut_speed": "fast", "transition": "jump_cut", "energy": "high"}', true, 1), + ('style-tpl-002', '质感慢镜', '慢节奏+电影感调色,适合美妆护肤珠宝', '{"cut_speed": "slow", "transition": "dissolve", "energy": "low", "color_grade": "cinematic"}', true, 2), + ('style-tpl-003', '口播种草', '数字人口播+产品特写穿插', '{"cut_speed": "medium", "transition": "cross_dissolve", "has_talking_head": true}', true, 3), + ('style-tpl-004', '场景叙事', '多场景切换+故事线叙述', '{"cut_speed": "medium", "transition": "wipe", "narrative": true}', true, 4) + """ + ) + + +def downgrade() -> None: + op.drop_table("viral_video_prompt_templates") + op.drop_table("viral_video_style_templates") + op.drop_table("viral_video_jobs") diff --git a/packages/adapters/sqlalchemy_impl/models.py b/packages/adapters/sqlalchemy_impl/models.py index 30bb923be..6bf6c825b 100755 --- a/packages/adapters/sqlalchemy_impl/models.py +++ b/packages/adapters/sqlalchemy_impl/models.py @@ -918,3 +918,71 @@ class GpuWorkerModel(Base): 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)) diff --git a/tests/unit/test_viral_video_models.py b/tests/unit/test_viral_video_models.py new file mode 100644 index 000000000..91f115178 --- /dev/null +++ b/tests/unit/test_viral_video_models.py @@ -0,0 +1,99 @@ +"""爆款视频 DB 模型单元测试(#2039 PR1:DB + migration)。 + +验证: +- 3 张新表可在内存 SQLite 上创建 +- 默认值与基本 CRUD 正常 +""" + +from __future__ import annotations + +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from packages.adapters.sqlalchemy_impl.models import ( + Base, + ViralVideoJobModel, + ViralVideoPromptTemplateModel, + ViralVideoStyleTemplateModel, +) + + +def _make_session(): + engine = create_engine("sqlite:///:memory:") + Base.metadata.create_all(engine) + return sessionmaker(bind=engine)() + + +class TestViralVideoJobModel: + def test_create_and_get(self): + session = _make_session() + job = ViralVideoJobModel( + id="job-001", + user_id="user-001", + images=["https://img.com/1.jpg"], + industry="美妆", + duration=60, + ) + session.add(job) + session.commit() + + fetched = session.query(ViralVideoJobModel).filter_by(id="job-001").one() + assert fetched.user_id == "user-001" + assert fetched.images == ["https://img.com/1.jpg"] + assert fetched.industry == "美妆" + assert fetched.duration == 60 + + def test_default_values(self): + session = _make_session() + job = ViralVideoJobModel(id="job-002", user_id="user-002") + session.add(job) + session.commit() + + fetched = session.get(ViralVideoJobModel, "job-002") + assert fetched.images == [] + assert fetched.fusion_level == "ai_polish" + assert fetched.style_strength == "medium" + assert fetched.status == "pending" + assert fetched.credits_cost == 0 + assert fetched.retry_count == 0 + assert fetched.style_guide is None + assert fetched.intent_result is None + + +class TestViralVideoStyleTemplateModel: + def test_create_and_get(self): + session = _make_session() + tpl = ViralVideoStyleTemplateModel( + id="tpl-001", + name="快节奏", + style_config={"cut_speed": "fast"}, + sort_order=1, + ) + session.add(tpl) + session.commit() + + fetched = session.get(ViralVideoStyleTemplateModel, "tpl-001") + assert fetched.name == "快节奏" + assert fetched.style_config == {"cut_speed": "fast"} + assert fetched.sort_order == 1 + + +class TestViralVideoPromptTemplateModel: + def test_create_and_get(self): + session = _make_session() + tpl = ViralVideoPromptTemplateModel( + id="pt-001", + prompt_type="image_analysis", + name="图片分析模板", + content="请分析图片:{image_url}", + variables=["image_url"], + ) + session.add(tpl) + session.commit() + + fetched = session.get(ViralVideoPromptTemplateModel, "pt-001") + assert fetched.prompt_type == "image_analysis" + assert fetched.content == "请分析图片:{image_url}" + assert fetched.variables == ["image_url"] + assert fetched.version == 1 + assert fetched.is_active is True -- 2.54.0 From fdeb792babb5194c44e10df154c6e725bf368ddd Mon Sep 17 00:00:00 2001 From: CI Bot Date: Tue, 29 Sep 2026 11:54:05 +0000 Subject: [PATCH 2/3] style: auto-format with black + isort + ruff + prettier [skip ci-format-check] --- alembic/versions/086_add_viral_video_tables.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/alembic/versions/086_add_viral_video_tables.py b/alembic/versions/086_add_viral_video_tables.py index 99e09d374..8f22364e7 100755 --- a/alembic/versions/086_add_viral_video_tables.py +++ b/alembic/versions/086_add_viral_video_tables.py @@ -84,16 +84,14 @@ def upgrade() -> None: ) # Seed 默认风格模板 - op.execute( - """ + op.execute(""" INSERT INTO viral_video_style_templates (id, name, description, style_config, is_system, sort_order) VALUES ('style-tpl-001', '快节奏冲击', '高频切镜+动感BGM,适合食品饮料等快消品', '{"cut_speed": "fast", "transition": "jump_cut", "energy": "high"}', true, 1), ('style-tpl-002', '质感慢镜', '慢节奏+电影感调色,适合美妆护肤珠宝', '{"cut_speed": "slow", "transition": "dissolve", "energy": "low", "color_grade": "cinematic"}', true, 2), ('style-tpl-003', '口播种草', '数字人口播+产品特写穿插', '{"cut_speed": "medium", "transition": "cross_dissolve", "has_talking_head": true}', true, 3), ('style-tpl-004', '场景叙事', '多场景切换+故事线叙述', '{"cut_speed": "medium", "transition": "wipe", "narrative": true}', true, 4) - """ - ) + """) def downgrade() -> None: -- 2.54.0 From 966da04c9cc52a11e680dac1e915346d7c7eee2e Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Tue, 29 Sep 2026 20:26:14 +0800 Subject: [PATCH 3/3] ci: retrigger pipeline (previous run stalled on runner) -- 2.54.0