Compare commits

...

12 Commits

Author SHA1 Message Date
xiaoxia 35f04f746b Merge pull request 'fix(viral-video): seed脚本 engine 获取修复' (#2283) from fix/viral-video-seed-script-engine into develop
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
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 / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 57s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (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
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 48s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m17s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m25s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m51s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m31s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m18s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m29s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m49s
CI/CD Pipeline / Integration Tests (push) Successful in 5m14s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 5m9s
CI/CD Pipeline / Validate - Style (push) Successful in 6m5s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m27s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m14s
AI Code Review / AI Code Review (pull_request) Successful in 7m1s
CI/CD Pipeline / Unit Tests (push) Successful in 13m17s
CI/CD Pipeline / Validate - Security (push) Successful in 17m18s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-10-12 21:22:44 +08:00
CI Bot 8ea3e26ab6 style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 0s
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 / PR Build API Image (pull_request) Successful in 1m41s
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 API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m2s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m47s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m34s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m52s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m50s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m17s
AI Code Review / AI Code Review (pull_request) Successful in 7m7s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 7m22s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 9m9s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 13m55s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m41s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 19m6s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m2s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 26s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m9s
2026-10-12 12:47:59 +00:00
CI Bot 769590a525 fix(seed): 补全 marketing_purpose/target_customer 两类默认seed数据(共52条)
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 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m51s
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 API Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
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 / 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
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 43s
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 / Validate - Security (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 - Style (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m3s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 2m37s
- marketing_purpose 20 条(对应前端 PURPOSES 枚举:获客引流/口播带货/对话短剧等)
- target_customer 8 条(对应前端 TARGET_CUSTOMERS 枚举:小镇青年/Z世代/精致妈妈等)
- seed总数:persona×12 + structure×5 + language×7 + marketing_purpose×20 + target_customer×8 = 52条
- 每条guide_text按该分类的创作指导风格撰写,前端选对应选项时精确命中DB返回专业hint
- 代码内关键词模糊兜底逻辑继续保留,未命中DB的自定义输入仍可正常工作
2026-10-12 20:37:45 +08:00
CI Bot 3df88c48fd fix(seed): 修复 viral_video_param_guides seed 脚本 engine 获取逻辑(_session_mod.engine 不存在导致 AttributeError)
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (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 / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
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 / 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
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m29s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m47s
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Successful in 5m28s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (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 / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
- 删除对 packages.adapters.sqlalchemy_impl.session.engine 的引用(session 模块只有 build_engine 工厂,无全局 engine)
- 改为与 seed_viral_video_prompts.py 一致:优先 DATABASE_URL 环境变量,fallback 到 get_shared_settings().database_url
- driver 统一为 postgresql+psycopg2://(与其他 seed 脚本一致)
- 不影响线上运行(代码内置 _DEFAULT_* 兜底),生产部署时必须用此修复版 seed
2026-10-12 20:33:56 +08:00
xiaoxia 6ab953ae2a Merge pull request 'refactor(viral-video): PR3 后端P2 统一异常+清理' (#2281) from refactor/viral-video-backend-p2-errors-and-cleanup into develop
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 1s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 5s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
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 / Build Staging Web Image (push) Successful in 1m7s
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 1m28s
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 / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m32s
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 / PR Build API Image (pull_request) Successful in 23s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 32s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 1m26s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m24s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 3m10s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m23s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m47s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 2m20s
CI/CD Pipeline / Validate - Style (push) Successful in 6m11s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m24s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m13s
AI Code Review / AI Code Review (pull_request) Successful in 7m0s
CI/CD Pipeline / Integration Tests (push) Failing after 8m18s
CI/CD Pipeline / Unit Tests (push) Successful in 13m29s
CI/CD Pipeline / Validate - Security (push) Successful in 19m22s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
2026-10-12 20:22:48 +08:00
xiaoxia 73294c6440 Merge pull request 'refactor(web/viral-video): PR6 前端P2 CSS瘦身+打磨' (#2282) from refactor/viral-video-frontend-p2-css-and-polish into develop
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
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 3s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 5s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 9s
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 Worker Image (pull_request) Successful in 1m13s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m25s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m57s
CI/CD Pipeline / Validate - Style (push) Has been cancelled
CI/CD Pipeline / Build Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Build Staging API Image (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Build Staging Web Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m23s
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Has been cancelled
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Retag skipped Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 5s
CI/CD Pipeline / Production Browser E2E (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
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 7m6s
2026-10-12 20:10:55 +08:00
xiaoxia c96b6de053 test(viral-video): fix mock leakage in p2_errors tests (monkeypatch restore)
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 1s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m38s
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 Worker Image (pull_request) Successful in 56s
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
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m57s
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
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m11s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 5m23s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 5m45s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m49s
AI Code Review / AI Code Review (pull_request) Successful in 6m36s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m11s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 15m11s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 15m5s
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 1m21s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 3m53s
2026-10-12 20:04:36 +08:00
xiaoxia 02660b063c Merge pull request 'ci(ditto): DITTO_EMOTION_TIMEOUT secret 注入部署渲染链路' (#2280) from fix/ditto-timeout-ci-config into develop
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
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 / Check push changed paths (push) Successful in 4s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Security (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (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 / 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 / PR Build API Image (pull_request) Successful in 55s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (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 / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 50s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m27s
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m3s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m19s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 3m50s
CI/CD Pipeline / Integration Tests (push) Successful in 4m53s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Retag skipped Staging API Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Web Image (push) Has been skipped
CI/CD Pipeline / Retag skipped Staging Worker Image (push) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 6m10s
AI Code Review / AI Code Review (pull_request) Successful in 7m4s
CI/CD Pipeline / Validate - Style (push) Successful in 7m36s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m45s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m45s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 4m23s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Unit Tests (push) Has been cancelled
CI/CD Pipeline / Validate - Security (push) Has been cancelled
CI/CD Pipeline / Build Production API Image (push) Has been cancelled
CI/CD Pipeline / Deploy Production (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / Build Production Web Image (push) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (push) Has been cancelled
CI/CD Pipeline / CI Gate (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
2026-10-12 20:01:33 +08:00
xiaoxia fd977991bd style(web): prettier --write 5 files (CI lint)
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 12s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m12s
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m54s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
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 Unit Tests (pull_request) Successful in 2m28s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 2m31s
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
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m53s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m19s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 10m49s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 11m25s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 16m8s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Successful in 1s
ACR Cleanup / ACR Image Cleanup (pull_request_target) Successful in 45s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 3m27s
2026-10-12 19:38:48 +08:00
CI Bot ce9da8cbfe style: auto-format with black + isort + ruff + prettier [skip ci-format-check]
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 9s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 13s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m16s
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 API Image (pull_request) Successful in 21s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m37s
AI Code Review / AI Code Review (pull_request) Successful in 6m44s
CI/CD Pipeline / Validate - Style (pull_request) Successful in 6m57s
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
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Successful in 6m41s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 6m43s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 14m39s
CI/CD Pipeline / Validate - Security (pull_request) Successful in 16m57s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / CI Gate (pull_request) Failing after 1s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 10m10s
2026-10-12 11:38:03 +00:00
agent e9128449ef refactor(viral-video): PR3 P2 errors+cleanup (统一异常/兜底文案/Seedance拆分/常量/param_guides DB)
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 4s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m3s
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 API Image (pull_request) Successful in 15s
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m2s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 43s
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
AI Code Review / AI Code Review (pull_request) Successful in 6m56s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration 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 / ACR Image Cleanup (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 - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
- 新建 packages/domain/viral_video/ 包(原 viral_video.py 拆为 entities.py + __init__.py 重导出)
- 新增 errors.py: ViralVideoError 基类 + Config/Upstream/Compliance/Resource/Credits/Timeout/LLM 7 个子类
- 新增 stages.py: STAGE_* / PROGRESS_* 阶段/进度常量集中维护
- worker viral_video.py 主路径 RuntimeError/ValueError → ViralVideo* 异常;
  顶层 except 拆 ViralVideoError 分支,用 user_message 做前端展示不打堆栈
- _fallback_script 去掉8条硬编码营销套话,改为"来看看{brand}家的{name}"等诚实短文案
- _step_video_analysis ImportError 不再返回占位假风格 dict,返回 None 下游跳过
- _assemble_seedance_prompt 拆分:抽取 _SEEDANCE_* / _WAN_* 常量 + _build_style_section + _build_character_section,主函数减 70+ 行,输出不变
- prompt_loader.py 4处静默 except 改为首次 warning(_warned set + 5min 冷却去重)
- 新建 packages/application/viral_video/param_guides.py: DB 读 viral_video_param_guides,30s TTL 缓存
- worker 5个 hint 函数(persona/structure/marketing_purpose/target_customer/language)改为 DB 优先 + _DEFAULT_* 代码兜底
- 新增迁移 109_add_viral_video_param_guides.py(5种 guide_type, UNIQUE(type,key))
- 新增 scripts/seed_viral_video_param_guides.py 幂等灌入默认值
- 新增 tests/unit/test_viral_video_p2_errors.py 6 个测试,修复既有测试引用 _PERSONA_STYLE_GUIDE → _DEFAULT_PERSONA_HINTS

测试:pytest tests/unit/ -k viral → 254 passed, 2 skipped
验收:worker/API import ok;alembic heads → 109 (head)
2026-10-12 19:25:48 +08:00
CI Bot 90ec76ce92 refactor(web/viral-video): PR6 P2清理 - CSS瘦身/inlie style迁移/抖音空功能删除/短剧提示条迁移/GENDER_LABEL修复/console清理
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 3s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 1m12s
CI/CD Pipeline / PR Build API Image (pull_request) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m23s
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been skipped
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 Unit Tests (pull_request) Successful in 1m18s
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 / PR Build Web Image (pull_request) Successful in 2m23s
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration 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 / ACR Image Cleanup (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
AI Code Review / AI Code Review (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
Preview Deploy / Deploy Preview Environment (pull_request) Has been cancelled
- GENDER_LABEL: 修复"女声·女声"/"男声·男声"重复拼接bug,改为"女声"/"男声"
- douyin空功能清理: 删除handleDouyinUrl空函数、抖音链接识别按钮/输入框UI、douyinInput state、VoicePanel中'douyin'分支、RefVideo.douyinUrl字段
- 短剧提示条: 从ViralVideoPage底部(grid外)迁移到Step2CopyPanel内部就地显示(选短剧类营销目的时显示),inline style抽为.vv-shortdrama-hint class,文案改为'💡 短剧模式已选中,对手戏角色将自动分配配角音色'
- copyError持久显示: 合规审核失败错误条从step3列移到Step2CopyPanel文案区持久显示(原只toast 3秒+step3列显示,不在文案区就近)
- CSS清理: 删除未使用class约25个(v1残留/旧结构),CSS 2274→2157行(-117行)
- inline style迁移CSS: speaker标签(.vv-sb-speaker-tag-main/guest)、spinner、voice空态、loading图标、margin/font-size helpers等静态多属性inline style抽为class
- console清理: 删除2处console.warn调试日志
- 不动API契约/三列等高布局/CSS Module,复用ErrorRetryBar/VVSpinner组件

tsc --noEmit: 0 error
2026-10-12 19:14:34 +08:00
25 changed files with 1302 additions and 406 deletions
@@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
"""viral_video_param_guides 表:运营可配置的人设/结构/营销目的/目标客群/语言指导文案(PR3 P2-h)。
Revision ID: 109_add_viral_video_param_guides
Revises: 108_storyboard_v4_multivoice
Create Date: 2026-10-12
表用途:
- 替代 worker 中 ~130 行硬编码 if/elif mapping;
- 5 类 guide_type: persona / structure / marketing_purpose / target_customer / language;
- 启动时由 seed_viral_video_param_guides.py 灌入代码默认值,运营可在后台改,30s TTL 缓存自动热加载。
"""
from alembic import op
revision = "109_add_viral_video_param_guides"
down_revision = "108_storyboard_v4_multivoice"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("""
CREATE TABLE IF NOT EXISTS viral_video_param_guides (
id SERIAL PRIMARY KEY,
guide_type VARCHAR(32) NOT NULL,
guide_key VARCHAR(64) NOT NULL,
guide_text TEXT NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (guide_type, guide_key)
)
""")
op.execute("""
CREATE INDEX IF NOT EXISTS ix_viral_video_param_guides_type_key
ON viral_video_param_guides (guide_type, guide_key)
""")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_viral_video_param_guides_type_key")
op.execute("DROP TABLE IF EXISTS viral_video_param_guides")
+91 -185
View File
@@ -138,27 +138,6 @@
border-radius: 12px;
overflow: hidden;
}
.vv-section-head {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
cursor: pointer;
user-select: none;
border-bottom: 1px solid #f0f0f5;
gap: 8px;
}
.vv-section.collapsed .vv-section-head {
border-bottom: none;
}
.vv-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #1f2937;
}
.vv-step-badge {
width: 22px;
height: 22px;
@@ -310,11 +289,6 @@
/* antd Select dropdown light */
.vv-page .ant-select-dropdown,
.vv-select-dropdown {
background: #fff !important;
border-radius: 8px !important;
padding: 4px !important;
}
.vv-page .ant-select-item {
color: #1f2937 !important;
border-radius: 6px !important;
@@ -655,10 +629,6 @@
margin-left: auto;
}
.vv-douyin-input {
margin-bottom: 8px;
}
.vv-ref-video-box {
margin-top: 8px;
}
@@ -669,44 +639,6 @@
max-height: 200px;
}
.vv-audio-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
.vv-audio-cell {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 14px 8px;
background: #fff;
border: 1px solid #e0e0e8;
border-radius: 8px;
color: #6b7280;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.vv-audio-cell:hover:not(:disabled) {
border-color: #7c3aed;
color: #7c3aed;
}
.vv-audio-cell.selected,
.vv-audio-cell.active {
background: #f5f0ff;
border-color: #7c3aed;
color: #7c3aed;
}
.vv-audio-cell:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.vv-audio-ic {
font-size: 20px;
}
.vv-selected-audio {
display: flex;
align-items: center;
@@ -857,12 +789,6 @@
color: #6b7280;
flex-shrink: 0;
}
.vv-recog-v {
color: #1f2937;
}
.vv-recog-meta {
color: #9ca3af;
}
/* progress mini */
.vv-progress-mini-inline {
@@ -1002,11 +928,6 @@
.vv-form-row {
margin-bottom: 10px;
}
.vv-form-hint {
font-size: 12px;
color: #9ca3af;
line-height: 1.4;
}
@media (max-width: 500px) {
.vv-form-grid {
grid-template-columns: 1fr;
@@ -1026,29 +947,6 @@
min-height: 260px;
overflow: hidden;
}
.vv-copy-head {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
.vv-copy-title {
font-size: 14px;
font-weight: 600;
color: #1f2937;
}
.vv-copy-tag {
font-size: 11px;
padding: 2px 8px;
border-radius: 10px;
font-weight: 400;
}
.vv-copy-tag-preview {
background: #fef3c7;
color: #b45309;
border: 1px solid #fcd34d;
margin-left: auto;
}
.vv-copy-loading {
display: flex;
@@ -1080,15 +978,6 @@
color: #6b7280;
}
.vv-copy-locked {
background: #fafafe;
border-color: #d8cafc;
}
.vv-copy-sub {
font-size: 12px;
color: #6b7280;
}
/* ── Storyboard (linear doc style) ── */
.vv-storyboard {
display: flex;
@@ -1789,84 +1678,10 @@
}
/* intent (keyword chips) - if used */
.vv-intent {
margin-top: 10px;
}
.vv-intent-title {
font-size: 12px;
color: #6b7280;
margin-bottom: 6px;
}
.vv-intent-row {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.vv-chip {
padding: 4px 10px;
background: #fff;
border: 1px solid #e0e0e8;
border-radius: 14px;
font-size: 12px;
color: #374151;
cursor: pointer;
}
.vv-chip.picked {
background: #f5f0ff;
color: #7c3aed;
border-color: #7c3aed;
}
/* misc helpers */
.vv-k {
color: #6b7280;
}
.vv-v {
color: #1f2937;
}
.vv-hint-yellow {
background: #fef3c7;
border: 1px solid #fcd34d;
color: #b45309;
font-size: 12px;
padding: 8px;
border-radius: 6px;
line-height: 1.6;
margin-top: 8px;
}
.vv-hint-gray {
color: #9ca3af;
font-size: 12px;
}
/* Segmented tabs (legacy) */
.vv-seg-tabs {
display: inline-flex;
background: #f5f6fa;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 3px;
gap: 2px;
}
.vv-seg-tab {
padding: 6px 14px;
border-radius: 6px;
background: #fff;
border: none;
color: #6b7280;
font-size: 12px;
cursor: pointer;
transition: all 0.15s;
}
.vv-seg-tab:hover {
color: #374151;
background: #fff;
}
.vv-seg-tab.active {
background: #f3f0ff;
color: #7c3aed;
font-weight: 500;
}
/* ── v1.6 我的音色(默认主路径) ── */
.vv-voice-section {
@@ -2272,3 +2087,94 @@
.vv-file-list {
max-height: none;
}
/* ── P2 新增 utility / 组件类 ── */
/* 短剧模式提示条(放在 Step2 区域就地显示) */
.vv-shortdrama-hint {
padding: 8px 12px;
margin-bottom: 8px;
background-color: #fff7e6;
border: 1px solid #ffd591;
border-radius: 4px;
font-size: 13px;
color: #d46b08;
}
/* speaker 标签(主角 / 配角) */
.vv-sb-speaker-tag {
display: inline-block;
padding: 2px 8px;
margin: 0 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
vertical-align: middle;
}
.vv-sb-speaker-tag-main {
background-color: #e6f7ff;
color: #1890ff;
}
.vv-sb-speaker-tag-guest {
background-color: #fff7e6;
color: #fa8c16;
}
.vv-sb-speaker-main {
color: #1890ff;
}
.vv-sb-speaker-guest {
color: #fa8c16;
}
/* 间距 / 排版 helpers */
.vv-mt-4 {
margin-top: 4px;
}
.vv-mt-6 {
margin-top: 6px;
}
.vv-mt-8 {
margin-top: 8px;
}
.vv-mb-6 {
margin-bottom: 6px;
}
.vv-mr-6 {
margin-right: 6px;
}
.vv-fs-12 {
font-size: 12px;
}
.vv-fs-13 {
font-size: 13px;
}
.vv-fs-22 {
font-size: 22px;
}
.vv-p-12 {
padding: 12px;
}
.vv-text-center {
text-align: center;
}
.vv-flex-1 {
flex: 1;
}
.vv-w-170 {
width: 170px;
flex: 0 0 auto;
}
.vv-inline-spinner {
margin-right: 6px;
display: inline-block;
vertical-align: middle;
}
.vv-progress-bar-sm {
margin: 6px 0 4px;
}
.vv-loading-ic {
font-size: 36px;
color: #7c3aed;
}
@@ -38,7 +38,6 @@ function emptyTask(id: string, title: string): TabTask {
title,
images: [],
refVideo: null,
douyinInput: "",
voicePanel: null,
refAudio: null,
fusionLevel: "ai_full",
@@ -176,7 +175,6 @@ const ViralVideoPage: React.FC = () => {
moveImg,
clearImages,
handleLocalRefVideo,
handleDouyinUrl,
removeRefVideo,
handleVoiceFile,
onCloneSuccess,
@@ -208,8 +206,8 @@ const ViralVideoPage: React.FC = () => {
try {
const job = await getViralVideoJob(ev.job_id)
onPollUpdate(job)
} catch (e) {
console.warn("[viral-video] WS终态主动拉取失败,等待HTTP轮询兜底", e)
} catch {
// WS终态拉取失败,等待HTTP轮询兜底
}
},
})
@@ -413,7 +411,6 @@ const ViralVideoPage: React.FC = () => {
<Step1UploadPanel
images={task.images}
refVideo={task.refVideo}
douyinInput={task.douyinInput}
voicePanel={task.voicePanel}
refAudio={task.refAudio}
voiceTipsOpen={task.voiceTipsOpen}
@@ -435,11 +432,6 @@ const ViralVideoPage: React.FC = () => {
onImagesSelected={appendImagesFromFiles}
onAnalyze={handleAnalyzeImages}
onPickLocalVideo={() => videoInputRef.current?.click()}
onToggleDouyinPanel={() =>
setTask({ voicePanel: task.voicePanel === "douyin" ? null : "douyin" })
}
onDouyinInputChange={(v) => setTask({ douyinInput: v })}
onDouyinSubmit={handleDouyinUrl}
onVideoSelected={handleLocalRefVideo}
onRemoveRefVideo={removeRefVideo}
onStyleStrengthChange={(v) => setTask({ styleStrength: v })}
@@ -473,6 +465,7 @@ const ViralVideoPage: React.FC = () => {
viralStructure={task.viralStructure}
marketingPurpose={task.marketingPurpose}
duration={task.duration}
copyError={task.copyError}
onFusionLevelChange={(v) => setTask({ fusionLevel: v })}
onUserCopyChange={(v) => setTask({ userCopy: v })}
onParamChange={(patch) => setTask(patch)}
@@ -529,31 +522,9 @@ const ViralVideoPage: React.FC = () => {
onRetry={handleRetry}
/>
)}
{/* REVIEW 合规失败就地显示 */}
{task.uiStep === "step2_copy_ready" && task.copyError && (
<ErrorRetryBar label="文案审核未通过" message={task.copyError} />
)}
</div>
</div>
{/* 短剧提示条 */}
{task.marketingPurpose && task.marketingPurpose.includes("短剧") && (
<div
style={{
padding: "8px 12px",
marginBottom: 8,
backgroundColor: "#fff7e6",
border: "1px solid #ffd591",
borderRadius: 4,
fontSize: 13,
color: "#d46b08",
}}
>
💡 短剧模式将自动为对手戏角色分配配角音色
</div>
)}
<CloneModal
open={cloneModalOpen}
onClose={() => setCloneModalOpen(false)}
@@ -152,8 +152,7 @@ export default function AssetPickerModal({
<div className="vv-modal-body">
<div className="vv-asset-search">
<select
className="vv-input"
style={{ width: 170, flex: "0 0 auto" }}
className="vv-input vv-w-170"
value={libraryId}
onChange={(e) => setLibraryId(e.target.value)}
disabled={loadingLibs || libraries.length === 0}
@@ -201,8 +200,8 @@ export default function AssetPickerModal({
</div>
{kind === "voice" ? (
<>
<div style={{ marginTop: 8, fontSize: 13 }}>暂无配音素材</div>
<div style={{ marginTop: 4, fontSize: 12, color: "#9ca3af" }}>
<div className="vv-mt-8 vv-fs-13">暂无配音素材</div>
<div className="vv-muted vv-mt-4 vv-fs-12">
请先在「配音/我的音色」中上传音频文件,或在素材库管理中添加
</div>
</>
@@ -106,10 +106,10 @@ const CATEGORY_LABELS: Record<string, string> = {
const GENDER_LABEL = (v: PresetVoice) => {
if (v.gender_label) return v.gender_label
const g = v.gender
if (g === "female") return "女声·女声"
if (g === "male") return "男声·男声"
if (g === "child") return "童声·童声"
return "性别未标注·其他"
if (g === "female") return "女声"
if (g === "male") return "男声"
if (g === "child") return "童声"
return "其他"
}
const AVATAR_BG = (gender?: string) => {
@@ -303,7 +303,7 @@ const PresetVoicePickerModal: React.FC<Props> = ({
{v.avatar_url ? (
<img src={v.avatar_url} alt={v.name} />
) : (
<UserOutlined style={{ fontSize: 22 }} />
<UserOutlined className="vv-fs-22" />
)}
</div>
<div className="vv-voice-card-name" title={v.name}>
@@ -5,9 +5,7 @@ import {
ArrowUpOutlined,
ArrowDownOutlined,
CloseOutlined,
LinkOutlined,
} from "@ant-design/icons"
import { Input } from "antd"
import { VVSpinner } from "./VVSpinner"
import { renderMarkdown } from "../utils/renderMarkdown"
import { CheckCircleFilled } from "@ant-design/icons"
@@ -21,7 +19,6 @@ import type { AssetItem } from "@/api/assets"
export interface Step1UploadPanelProps {
images: UploadedImage[]
refVideo: RefVideo
douyinInput: string
voicePanel: VoicePanel
refAudio: RefAudio
voiceTipsOpen: boolean
@@ -45,9 +42,6 @@ export interface Step1UploadPanelProps {
onAnalyze: () => void
// callbacks - ref video
onPickLocalVideo: () => void
onToggleDouyinPanel: () => void
onDouyinInputChange: (v: string) => void
onDouyinSubmit: () => void
onVideoSelected: (file: File) => void
onRemoveRefVideo: () => void
onStyleStrengthChange: (v: StyleStrength) => void
@@ -94,10 +88,7 @@ function RecognitionBlock({
return (
<div className="vv-recog">
<div className="vv-recog-title">
<VVSpinner
color="#7c3aed"
style={{ marginRight: 6, display: "inline-block", verticalAlign: "middle" }}
/>
<VVSpinner color="#7c3aed" className="vv-inline-spinner" />
识别描述汇览
</div>
<div className="vv-muted">AI 正在识别画面…</div>
@@ -240,9 +231,6 @@ export const Step1UploadPanel: React.FC<Step1UploadPanelProps> = (p) => {
<button className="vv-action-btn" onClick={p.onPickLocalVideo}>
<UploadOutlined /> 本地上传视频
</button>
<button className="vv-action-btn" onClick={p.onToggleDouyinPanel}>
<LinkOutlined /> 抖音链接识别
</button>
</div>
<input
ref={p.videoInputRef}
@@ -254,24 +242,6 @@ export const Step1UploadPanel: React.FC<Step1UploadPanelProps> = (p) => {
e.target.value = ""
}}
/>
{p.voicePanel === "douyin" && !p.refVideo && (
<div className="vv-douyin-input">
<Input
placeholder="粘贴抖音分享链接,例:https://v.douyin.com/xxxxx/"
value={p.douyinInput}
onChange={(e) => p.onDouyinInputChange(e.target.value)}
onPressEnter={p.onDouyinSubmit}
suffix={
<button className="vv-link-btn" onClick={p.onDouyinSubmit}>
识别
</button>
}
/>
<div className="vv-muted" style={{ marginTop: 4, fontSize: 11 }}>
粘贴后自动解析下载参考视频(敬请期待)
</div>
</div>
)}
{p.refVideo && (
<div className="vv-ref-video-box">
<video
@@ -279,10 +249,10 @@ export const Step1UploadPanel: React.FC<Step1UploadPanelProps> = (p) => {
controls
className="vv-ref-video-el"
/>
<div className="vv-file-info" style={{ marginTop: 6 }}>
<div className="vv-file-info vv-mt-6">
<div className="vv-file-name">{p.refVideo.name}</div>
{p.refVideo.uploading && (
<div className="vv-progress-bar" style={{ margin: "6px 0 4px" }}>
<div className="vv-progress-bar vv-progress-bar-sm">
<div className="vv-progress-fill" style={{ width: `${p.refVideo.pct}%` }} />
</div>
)}
@@ -292,9 +262,7 @@ export const Step1UploadPanel: React.FC<Step1UploadPanelProps> = (p) => {
</button>
{p.refVideo.ossUrl && (
<div className="vv-mt-8">
<div className="vv-label" style={{ marginBottom: 6 }}>
风格借鉴强度
</div>
<div className="vv-label vv-mb-6">风格借鉴强度</div>
<div className="vv-pill-row">
{STYLE_STRENGTHS.map((s) => (
<button
@@ -4,6 +4,7 @@ import { Select } from "antd"
import { FUSION_LEVELS } from "@/api/viral-video/types"
import type { FusionLevel } from "@/api/viral-video/types"
import { VVSpinner } from "./VVSpinner"
import { ErrorRetryBar } from "./ErrorRetryBar"
import { StoryboardCard } from "./StoryboardCard"
import type { SbDoc, StoryboardEditor } from "../hooks/useStoryboardEditor"
import type { UploadedImage, UIStep, ViralVideoJob } from "../types"
@@ -52,6 +53,7 @@ export interface Step2CopyPanelProps {
images: UploadedImage[]
locked: boolean
job: ViralVideoJob | null
copyError?: string
}
export const Step2CopyPanel: React.FC<Step2CopyPanelProps> = (p) => {
@@ -181,6 +183,16 @@ export const Step2CopyPanel: React.FC<Step2CopyPanelProps> = (p) => {
</div>
</div>
{/* 短剧模式提示 */}
{p.marketingPurpose && p.marketingPurpose.includes("短剧") && (
<div className="vv-shortdrama-hint">
💡 短剧模式已选中,对手戏角色将自动分配配角音色
</div>
)}
{/* 文案审核错误(持久显示) */}
{p.copyError && <ErrorRetryBar label="文案审核未通过" message={p.copyError} />}
{/* 分镜结果区 / 流式预览 */}
<StoryboardCard
uiStep={p.uiStep}
@@ -154,7 +154,7 @@ export const Step3VideoPanel: React.FC<Step3VideoPanelProps> = (p) => {
) : isGenerating ? (
<div className="vv-preview-placeholder">
<div className="ph-icon">
<LoadingOutlined style={{ fontSize: 36, color: "#7c3aed" }} />
<LoadingOutlined className="vv-loading-ic" />
</div>
<div className="ph-txt">
AI 正在生成视频中…
@@ -162,9 +162,7 @@ export const Step3VideoPanel: React.FC<Step3VideoPanelProps> = (p) => {
预计 1-3 分钟,请勿关闭页面
</div>
{getJobPhaseMessage(p.job) && (
<div className="vv-muted" style={{ marginTop: 6, fontSize: 12 }}>
{getJobPhaseMessage(p.job)}
</div>
<div className="vv-muted vv-mt-6 vv-fs-12">{getJobPhaseMessage(p.job)}</div>
)}
</div>
) : (
@@ -39,7 +39,7 @@ function renderVoiceoverWithSpeakers(text: string) {
const isMain = MAIN_SPEAKERS.includes(speaker)
elements.push(
<span key={i}>
<strong style={{ color: isMain ? "#1890ff" : "#fa8c16" }}>{speaker}</strong>
<strong className={isMain ? "vv-sb-speaker-main" : "vv-sb-speaker-guest"}>{speaker}</strong>
<span>{segText}</span>
</span>,
)
@@ -111,7 +111,7 @@ export const StoryboardCard: React.FC<StoryboardCardProps> = ({
if (uiStep === "step2_copy_ready" || uiStep === "step3_generating" || uiStep === "step3_done") {
return (
<div className="vv-copy-box vv-copy-loading">
<span style={{ color: "#9ca3af" }}>暂无分镜数据</span>
<span className="vv-muted">暂无分镜数据</span>
</div>
)
}
@@ -51,18 +51,7 @@ export const StoryboardShotItem: React.FC<StoryboardShotItemProps> = ({
{/* 说话人标签(有 speaker 时展示,纯展示不加编辑) */}
{shot.speaker && (
<span
className="vv-sb-speaker-tag"
style={{
display: "inline-block",
padding: "2px 8px",
marginRight: 8,
marginLeft: 8,
borderRadius: 4,
fontSize: 12,
fontWeight: 500,
backgroundColor: isMainSpeaker ? "#e6f7ff" : "#fff7e6",
color: isMainSpeaker ? "#1890ff" : "#fa8c16",
}}
className={`vv-sb-speaker-tag ${isMainSpeaker ? "vv-sb-speaker-tag-main" : "vv-sb-speaker-tag-guest"}`}
>
{shot.speaker}
</span>
@@ -143,8 +143,8 @@ export const VoicePickerSection: React.FC<VoicePickerSectionProps> = (p) => {
/>
{p.refAudio && (
<div className="vv-selected-audio">
<span style={{ marginRight: 6 }}>🔊</span>
<span style={{ flex: 1 }}>
<span className="vv-mr-6">🔊</span>
<span className="vv-flex-1">
已选:
{p.refAudio.source === "my_voice"
? `我的音色 · ${p.refAudio.name}`
@@ -168,9 +168,7 @@ export const VoicePickerSection: React.FC<VoicePickerSectionProps> = (p) => {
</button>
</div>
{p.voiceAssets.length === 0 ? (
<div className="vv-muted" style={{ padding: 12, textAlign: "center" }}>
暂无已上传的配音文件
</div>
<div className="vv-muted vv-text-center vv-p-12">暂无已上传的配音文件</div>
) : (
<div className="vv-voice-list">
{p.voiceAssets.map((v) => (
@@ -58,7 +58,6 @@ function emptyTask(id: string, title: string): TabTask {
title,
images: [],
refVideo: null,
douyinInput: "",
voicePanel: null,
refAudio: null,
fusionLevel: "ai_full",
@@ -27,7 +27,6 @@ export interface MediaActions {
removeImg: (localId: string) => void
moveImg: (idx: number, dir: -1 | 1) => void
handleLocalRefVideo: (file: File) => void
handleDouyinUrl: () => void
removeRefVideo: () => void
handleVoiceFile: (file: File) => void
onCloneSuccess: (v: VoiceClone | { id?: string; name?: string; voice_id?: string | null }) => void
@@ -221,20 +220,6 @@ export function useViralVideoMedia(opts: UseViralVideoMediaOptions): MediaAction
[setTask, uploadFile],
)
const handleDouyinUrl = () => {
const url = task.douyinInput.trim()
if (!url) {
message.warning("请粘贴抖音分享链接")
return
}
if (!/(douyin\.com|iesdouyin\.com)/.test(url)) {
message.warning("请输入有效的抖音链接")
return
}
message.info("抖音链接识别功能即将上线,当前请使用本地上传参考视频")
setTask({ douyinInput: "" })
}
const removeRefVideo = () => {
setTask((t) => {
if (t.refVideo?.url.startsWith("blob:")) URL.revokeObjectURL(t.refVideo.url)
@@ -350,7 +335,6 @@ export function useViralVideoMedia(opts: UseViralVideoMediaOptions): MediaAction
removeImg,
moveImg,
handleLocalRefVideo,
handleDouyinUrl,
removeRefVideo,
handleVoiceFile,
onCloneSuccess,
@@ -61,7 +61,7 @@ export function useViralVideoPolling(
failCountRef.current += 1
if (stoppedRef.current) return
if (failCountRef.current >= 20) {
console.warn("[viral-video] 轮询连续失败 20 次,停止轮询,请刷新页面重试")
// 轮询连续失败 20 次,停止轮询
stop()
return
}
+2 -4
View File
@@ -33,14 +33,13 @@ export type UploadedImage = {
}
export type RefVideo = {
source: "local" | "douyin"
source: "local"
url: string
name: string
uploading: boolean
pct: number
ossUrl?: string
file?: File
douyinUrl?: string
} | null
export type RefAudio = {
@@ -52,7 +51,7 @@ export type RefAudio = {
ttsVoiceId?: string
} | null
export type VoicePanel = "my_voices" | "preset" | "upload" | "library" | "record" | "douyin" | null
export type VoicePanel = "my_voices" | "preset" | "upload" | "library" | "record" | null
// ── 分镜脚本数据模型(新后端 copy_result 结构) ──
export interface StoryboardShot {
@@ -82,7 +81,6 @@ export type TabTask = {
// 素材
images: UploadedImage[]
refVideo: RefVideo
douyinInput: string
voicePanel: VoicePanel
refAudio: RefAudio
// STEP2 参数
+193 -91
View File
@@ -47,12 +47,19 @@ from packages.application.viral_video.constants import (
get_duration_spec,
)
from packages.application.viral_video.constants import get_expected_shot_count as _const_expected_shot_count
from packages.application.viral_video.param_guides import get_param_guide
from packages.application.viral_video.prompts import COMPLIANCE_BLACKLIST
from packages.domain.viral_video import (
STAGE_LABELS,
ViralVideoComplianceError,
ViralVideoError,
ViralVideoJob,
ViralVideoLLMError,
ViralVideoResourceError,
ViralVideoStage,
ViralVideoStatus,
ViralVideoTimeoutError,
ViralVideoUpstreamError,
)
logger = logging.getLogger(__name__)
@@ -516,6 +523,49 @@ def with_viral_job(
return func(job, repo, session, *rest_args, **kwargs)
except Retry:
raise
except ViralVideoError as e:
# PR3 P2(b): 业务异常走友好 message,不打堆栈
logger.warning(
"[viral] task %s business_error code=%s user_msg=%s detail=%s",
func.__name__,
e.code,
e.user_message,
e.detail,
)
err_user_msg = e.user_message
err_stage = (
getattr(e, "stage", "") if isinstance(e, (ViralVideoTimeoutError, ViralVideoLLMError)) else ""
)
# refund_on_failure: 业务异常同样按阶段3退款
if refund_on_failure and job is None:
try:
if session is not None:
_repo_safe = SQLAlchemyViralVideoJobRepository(session)
_job_safe = _repo_safe.get(job_id)
if _job_safe is not None and float(getattr(_job_safe, "credits_prepaid", 0) or 0) > 0:
_try_refund_viral_video(_job_safe)
_repo_safe.update(_job_safe)
except Exception:
logger.exception("[viral] task %s 兜底退款异常", func.__name__)
elif refund_on_failure and job is not None:
try:
if float(getattr(job, "credits_prepaid", 0) or 0) > 0:
_try_refund_viral_video(job)
except Exception:
logger.exception("[viral] task %s 兜底退款异常", func.__name__)
_err_job = job
try:
if job is None and session is not None:
_repo_tmp = SQLAlchemyViralVideoJobRepository(session)
_err_job = _repo_tmp.get(job_id)
if _err_job is not None and getattr(_err_job, "current_stage", None) and not err_stage:
err_stage = _err_job.current_stage
except Exception:
pass
_mark_failed_and_notify(
job_id, session, None, _err_job if _err_job is not None else None, err_user_msg, err_stage
)
return {"ok": False, "job_id": job_id, "error": err_user_msg, "code": e.code}
except Exception as e:
logger.exception("[viral] task %s failed: %s", func.__name__, e)
# refund_on_failure: 阶段3(render)失败兜底退款
@@ -749,22 +799,16 @@ def _step_video_analysis(job: ViralVideoJob) -> dict | None:
style_guide = analyze_video_style(video_url)
return style_guide if isinstance(style_guide, dict) else None
except ImportError as e:
logger.info("[爆款视频] video_analyzer 模块未就绪(%s),使用占位风格分析", e)
return {
"cut_speed": "medium",
"transition": "cross_dissolve",
"energy": "medium",
"color_grade": "neutral",
"narrative": False,
"source": "placeholder",
}
logger.info("[爆款视频] video_analyzer 模块未就绪(%s),跳过参考视频风格分析", e)
return None
except Exception as e:
logger.error("[爆款视频] 视频风格分析失败: %s", e)
return {"error": str(e), "source": "failed"}
# persona_id -> 文案风格指导(仅用于给脚本生成 LLM 的风格提示)
_PERSONA_STYLE_GUIDE: dict[str, str] = {
# ── PR3 P2(h): 参数指导优先查 DB(viral_video_param_guides),代码默认值兜底 ──
# 代码默认值保证 DB 未 seed / DB 异常时不阻断流程
_DEFAULT_PERSONA_HINTS: dict[str, str] = {
"宝妈": "温柔贴心,多从带娃和家庭实用角度分享,语气亲切有共鸣",
"店主": "热情实在,像当面招呼客人,突出靠谱和实在优惠",
"专业顾问": "专业可信,讲清原理和效果,用事实打消顾虑",
@@ -779,19 +823,7 @@ _PERSONA_STYLE_GUIDE: dict[str, str] = {
"育儿师": "科学育儿角度,温柔坚定,给具体可操作的建议",
}
def _persona_style_hint(persona_id: str) -> str:
"""根据 persona_id 查文案风格指导;未命中/空值返回通用提示。"""
pid = (persona_id or "").strip()
if pid in _PERSONA_STYLE_GUIDE:
return f"【人设风格:{pid}】{_PERSONA_STYLE_GUIDE[pid]}"
if pid:
# 前端传了自由值,照直提示,不阻塞
return f"【人设风格:{pid}】按该人设的口吻、话术习惯组织口播和出镜动作"
return "【人设风格:未指定】亲切自然、像朋友分享好物"
_VIRAL_STRUCTURE_GUIDE: dict[str, str] = {
_DEFAULT_VIRAL_STRUCTURE_HINTS: dict[str, str] = {
"反差破局+亮明观点+还原现状": "开头3秒用反差/痛点钩子抓注意力,中段亮出核心卖点或观点,结尾还原真实到店/使用场景引导行动",
"痛点切入+方案展示+效果对比": "开头直击用户痛点场景,中间展示产品/服务解决方案,结尾用前后对比强化效果",
"故事引入+产品种草+行动引导": "用一个真实小故事/案例引入,自然过渡到产品种草,结尾明确引导用户下一步行动",
@@ -800,81 +832,92 @@ _VIRAL_STRUCTURE_GUIDE: dict[str, str] = {
}
def _db_guide(guide_type: str, guide_key: str) -> str | None:
"""读 DB 指导文本;DB 异常返回 None(不抛异常、不打刷屏日志)。"""
try:
return get_param_guide(guide_type, guide_key)
except Exception: # noqa: BLE001
return None
def _persona_style_hint(persona_id: str) -> str:
"""根据 persona_id 查文案风格指导;未命中/空值返回通用提示。"""
pid = (persona_id or "").strip()
if not pid:
return "【人设风格:未指定】亲切自然、像朋友分享好物"
text = _db_guide("persona", pid) or _DEFAULT_PERSONA_HINTS.get(pid)
if text:
return f"【人设风格:{pid}】{text}"
return f"【人设风格:{pid}】按该人设的口吻、话术习惯组织口播和出镜动作"
def _viral_structure_hint(structure: str) -> str:
"""根据 viral_structure 映射具体写作指导;未命中返回通用提示。"""
s = (structure or "").strip()
if s in _VIRAL_STRUCTURE_GUIDE:
return f"【爆款结构:{s}】{_VIRAL_STRUCTURE_GUIDE[s]}"
if s:
return f"【爆款结构:{s}】按该结构编排内容节奏和叙事逻辑"
return "【爆款结构:未指定】自由组织,保证开头有钩子、中段有卖点、结尾有行动引导"
if not s:
return "【爆款结构:未指定】自由组织,保证开头有钩子、中段有卖点、结尾有行动引导"
text = _db_guide("structure", s) or _DEFAULT_VIRAL_STRUCTURE_HINTS.get(s)
if text:
return f"【爆款结构:{s}】{text}"
return f"【爆款结构:{s}】按该结构编排内容节奏和叙事逻辑"
def _marketing_purpose_hint(mp: str) -> str:
"""根据营销目的提供创作指导 hint。
覆盖前端 20 个 PURPOSES,分 4 类指导:
- 短剧类:多角色对话,格式"角色:台词",禁止旁白,角色间镜头切换
- 口播类:第一人称对镜头说话,真人出镜感
- 门店类:店主或探店博主出镜讲解+场景展示
- 品牌类:主演出镜说话或角色对白,禁止上帝视角旁白
"""
"""根据营销目的提供创作指导 hint(关键词分类,DB 精确覆盖优先)。"""
mp = (mp or "").strip()
if not mp:
return ""
# 精确匹配 DB 覆盖(运营可配置自定义营销目的)
db_text = _db_guide("marketing_purpose", mp)
if db_text:
return db_text
# 短剧类(多角色对话)
if any(k in mp for k in ["对话短剧", "反转短剧", "悬念短剧", "情绪短片"]):
return "【短剧模式】必须双/多角色对话格式,台词用'角色:xxx'格式,镜头在角色间切换,禁止第三人称旁白解说。"
# 口播类(第一人称对镜头说话)
if any(
k in mp
for k in ["口播带货", "促销转化", "功能演示", "痛点解决", "获客引流", "账号涨粉", "活动通知", "场景种草"]
):
return "【口播模式】第一人称对镜头说话,像真人出镜,有语气停顿和情绪,禁止第三人称旁白。"
# 门店类(店主/探店博主出镜)
if any(k in mp for k in ["门店发现", "到店实录", "招牌体验", "同城团购"]):
return "【门店模式】店主或探店博主出镜讲解+场景展示,第一人称或角色对白,禁止上帝视角旁白。"
# 品牌类(主演出镜或角色对白)
if any(k in mp for k in ["品牌主张", "品牌故事", "生活方式", "创意概念"]):
return "【品牌模式】主演出镜说话或角色对白,禁止上帝视角旁白,要有品牌调性和情感共鸣。"
# 未匹配的营销目的:通用 hint
return "【通用模式】避免上帝视角旁白,优先第一人称或角色对话。"
def _target_customer_hint(tc: str) -> str:
"""根据目标客户提供台词风格指导。"""
"""根据目标客户提供台词风格指导(关键词分类,DB 精确覆盖优先)。"""
tc = (tc or "").strip()
if not tc:
return ""
# 年轻人
db_text = _db_guide("target_customer", tc)
if db_text:
return db_text
if any(k in tc for k in ["18-25", "年轻", "学生", "Z世代", "95后", "00后"]):
return "【目标年轻客群】台词要活泼、有梗、节奏快,可用网络流行语。"
# 中年人
if any(k in tc for k in ["30-45", "中年", "家庭", "宝妈", "职场"]):
return "【目标中年客群】台词要实用、有共鸣,强调性价比和品质。"
# 老年人
if any(k in tc for k in ["50+", "老年", "退休", "银发"]):
return "【目标老年客群】台词要清晰、慢节奏,强调健康和实惠。"
# 高端
if any(k in tc for k in ["高端", "商务", "精英", "白领"]):
return "【目标高端客群】台词要专业、有格调,强调品质和身份。"
return ""
def _language_hint(language: str) -> str:
"""根据 language 代码返回语言提示。"""
"""根据 language 代码返回语言提示(DB 精确覆盖优先,代码默认兜底)。"""
lang = (language or "zh-CN").strip().lower()
mapping = {
db_text = _db_guide("language", lang)
if db_text:
return f"【语言:{lang}】{db_text}"
_defaults: dict[str, str] = {
"zh-cn": "使用标准普通话,口语化表达",
"zh-tw": "使用台湾腔中文,语气温柔亲切",
"zh-hk": "使用粤语风格中文表达",
@@ -883,7 +926,7 @@ def _language_hint(language: str) -> str:
"ja-jp": "使用日语,自然口语化",
"ko-kr": "使用韩语,亲切自然",
}
hint = mapping.get(lang, "")
hint = _defaults.get(lang, "")
if hint:
return f"【语言:{lang}】{hint}"
if lang.startswith("zh"):
@@ -988,25 +1031,15 @@ def _fallback_script(job: ViralVideoJob) -> dict:
_brand_str = _brands[0] if _brands else ""
_mp = (getattr(job, "marketing_purpose", "") or "").strip()
# 基于真实识别结果构建口播,不再用"这家店""一款好物"等通用表述
# PR3 P2(c): 兜底口播只用真实 VLM 输入,诚实简短,让前端/用户一眼看出是兜底触发重试
if job.user_copy_text:
voiceover = job.user_copy_text
elif _dominant == "store" and _name_str:
voiceover = (
f"今天带大家来{_name_str}看看,{_brand_str + ',' if _brand_str else ''}环境和服务都很赞,推荐来体验!"
)
elif _dominant == "store":
voiceover = "今天带大家探店,这家店的环境和服务都很不错,推荐大家来体验一下。"
elif _dominant == "product" and _name_str:
voiceover = f"给大家分享{_brand_str + '家的' if _brand_str else ''}{_name_str},真心不错,推荐你们试试。"
elif _dominant == "product":
voiceover = "给大家分享一款我最近在用的好物,真的很不错,推荐你们也试试。"
elif _dominant == "person":
voiceover = "哈喽,今天给大家分享我的日常,简单舒适又好看,你们觉得怎么样?"
elif _dominant == "scene" and _name_str:
voiceover = f"带大家感受一下{_name_str},氛围感拉满,真的很适合打卡体验。"
elif _brand_str and _name_str:
voiceover = f"来看看{_brand_str}家的{_name_str}"
elif _name_str:
voiceover = f"来看看{_name_str}"
else:
voiceover = "带大家感受一下这个空间,氛围感拉满,真的很适合打卡体验。"
voiceover = "今天分享一个好东西"
# 口播超长时截断(字数上限统一从 constants 取)
_max_v = get_duration_spec(dur)["max_chars"]
if len(voiceover) > _max_v:
@@ -1699,11 +1732,55 @@ def _finalize_fallback_script(job: ViralVideoJob) -> dict:
return fb
# ── PR3 P2(e): Seedance/Wan prompt 常量与辅助函数(从 _assemble_seedance_prompt 中抽出) ──
_SEEDANCE_OVERVIEW_HEADER = "【视频总览】"
_SEEDANCE_REFS_HEADER = "【参考素材】"
_SEEDANCE_SCRIPT_HEADER = "【分镜脚本】"
_SEEDANCE_HARD_HEADER = "【硬性约束】"
_SEEDANCE_NEG_HEADER = "【负面提示词】"
_SEEDANCE_DEFAULT_SCENE = "专业、清晰、明亮有质感"
_SEEDANCE_DURATION_FOOT = "(单次生成,时长严格匹配)"
_SEEDANCE_NEG_JOINER = ","
_WAN_HEAD_SCENE_FALLBACK = "画面清晰有质感"
_WAN_STYLE_LABEL = "风格说明:"
_WAN_SHOT_PREFIX = "第{n}个镜头[{label}]"
def _build_style_section(style_guide: dict | None, scene_lighting: str) -> str:
"""从 style_guide / scene_lighting 生成整体风格描述文本。"""
if not isinstance(style_guide, dict):
return scene_lighting or _SEEDANCE_DEFAULT_SCENE
parts: list[str] = []
if scene_lighting:
parts.append(scene_lighting)
for k in ("color_grade", "energy", "cut_speed", "transition"):
v = style_guide.get(k)
if v and v != "placeholder":
parts.append(f"{k}={v}")
if style_guide.get("narrative"):
parts.append("叙事感强")
return ",".join(parts) if parts else _SEEDANCE_DEFAULT_SCENE
def _build_character_section(person_portraits, pre_trusted_images, trusted_urls) -> str:
"""人物参考段落。当前版本人物参考走 reference_images 通道,
不在 prompt 正文重复描述;保留接口供后续扩展,当前返回空串。
"""
# person_portraits / pre_trusted_images / trusted_urls 目前由 call_video_generation
# 通过 reference_images / pre_trusted_images 参数直接传 Seedance,不在文本 prompt
# 中重复声明以避免冲突。此处保留函数入口以匹配设计文档,未来可扩展肖像一致性文本提示。
return ""
def _assemble_seedance_prompt(copy_result: dict, job: ViralVideoJob) -> str:
"""把编导脚本拼成符合官方推荐格式的 prompt(按 provider 分发)。
- doubao (Seedance): [X-Y秒] 单行时间戳 + 镜头级 @图片N 绑定
- dashscope (Wan 3.0): 第N个镜头[X-Y秒] 单行格式
PR3 P2(e): 字符串常量已抽取到模块级 _SEEDANCE_* / _WAN_*,
风格段与人物段通过 _build_style_section / _build_character_section 组装,
输出内容与旧版完全一致。
"""
from packages.domain.points_rules import get_viral_video_model_config
@@ -1719,6 +1796,7 @@ def _assemble_seedance_prompt(copy_result: dict, job: ViralVideoJob) -> str:
hc = copy_result.get("hard_constraints") or _DEFAULT_HARD_CONSTRAINTS
np_list = copy_result.get("negative_prompts") or _DEFAULT_NEGATIVE_PROMPTS
images = list(job.images or [])
style_guide = job.style_guide if isinstance(getattr(job, "style_guide", None), dict) else None
model = getattr(job, "video_model", "") or None
provider = get_viral_video_model_config(model).get("provider", "doubao")
@@ -1730,9 +1808,15 @@ def _assemble_seedance_prompt(copy_result: dict, job: ViralVideoJob) -> str:
return idx
return 0 if images else None
# 整体风格描述(复用 style_guide 如果有)
style_text = _build_style_section(style_guide, scene_lighting)
# 人物段(当前为空实现,仅保留扩展点,保持输出不变)
# 未来可注入 person_portraits/pre_trusted 提示,目前人物参考走 reference_images 通道
_char_text = _build_character_section([], getattr(job, "pre_trusted_images", None) or [], [])
if provider == "dashscope":
# ========== Wan 3.0 格式 ==========
head = f"{theme},{scene_lighting or '画面清晰有质感'},总时长{total_duration}秒。"
head = f"{theme},{style_text or _WAN_HEAD_SCENE_FALLBACK},总时长{total_duration}秒。"
body: list[str] = [head, ""]
for i, s in enumerate(shots):
tr = str(s.get("time_range", ""))
@@ -1740,40 +1824,37 @@ def _assemble_seedance_prompt(copy_result: dict, job: ViralVideoJob) -> str:
label = f"{pr[0]}-{pr[1]}秒" if pr else tr
cam = str(s.get("shot_type_angle_movement", "") or "").strip()
voice = str(s.get("voiceover", "") or "").strip()
pieces = [f"第{i + 1}个镜头[{label}]"]
pieces = [_WAN_SHOT_PREFIX.format(n=i + 1, label=label)]
if cam:
pieces.append(f"运镜:{cam}")
pieces.append(f"画面:{_shot_visual(s)}")
if voice:
pieces.append(f'配音:"{voice}"')
# Wan 通过 input.media 数组顺序隐式引用图1/图2,不写@图片
body.append(" ".join(pieces))
style_parts = [str(x) for x in hc if x] + [str(x) for x in np_list if x]
if style_parts:
body.append("")
body.append("风格说明:" + ";".join(style_parts[:6]))
body.append(_WAN_STYLE_LABEL + ";".join(style_parts[:6]))
return "\n".join(body)
# ========== Seedance 格式 ==========
lines: list[str] = []
lines.append("【视频总览】")
lines.append(_SEEDANCE_OVERVIEW_HEADER)
lines.append(f"- 主题:{theme}")
lines.append(f"- 总时长:{total_duration}秒(单次生成,时长严格匹配)")
lines.append(f"- 总时长:{total_duration}秒{_SEEDANCE_DURATION_FOOT}")
lines.append(f"- 画幅:{aspect_ratio}")
lines.append(f"- 整体风格:{scene_lighting or '专业、清晰、明亮有质感'}")
lines.append(f"- 整体风格:{style_text}")
lines.append("")
# 【参考素材】按镜头绑定
if images:
lines.append("【参考素材】")
lines.append(_SEEDANCE_REFS_HEADER)
for i in range(len(shots)):
ri = _ref(i)
if ri is not None:
lines.append(f"镜头{i + 1}参考@图片{ri + 1}")
lines.append("")
# 单行时间戳分镜
lines.append("【分镜脚本】")
lines.append(_SEEDANCE_SCRIPT_HEADER)
for i, s in enumerate(shots):
tr = str(s.get("time_range", ""))
pr = _parse_shot_seconds(tr)
@@ -1795,12 +1876,12 @@ def _assemble_seedance_prompt(copy_result: dict, job: ViralVideoJob) -> str:
lines.append(" ".join(pieces))
lines.append("")
lines.append("【硬性约束】")
lines.append(_SEEDANCE_HARD_HEADER)
for c in hc:
lines.append(f"- {c}")
lines.append("")
lines.append("【负面提示词】")
lines.append(",".join([str(x) for x in np_list if x]))
lines.append(_SEEDANCE_NEG_HEADER)
lines.append(_SEEDANCE_NEG_JOINER.join([str(x) for x in np_list if x]))
return "\n".join(lines)
@@ -1935,14 +2016,22 @@ def _step_render(job: ViralVideoJob, copy_result: dict, tts_audio_url: str | Non
user_msg,
(detail or "")[:500],
)
raise RuntimeError(err_msg)
raise ViralVideoUpstreamError(
provider=err_provider,
upstream_code=str(err_code),
user_message=err_msg,
detail=(detail or "")[:1000] or None,
status_code=int(status_code) if isinstance(status_code, int) and status_code else None,
)
if not result or not isinstance(result, dict):
_check_and_reraise(result)
video_path = result.get("video_path") or ""
usage = result.get("usage")
if not video_path or not Path(video_path).exists() or Path(video_path).stat().st_size == 0:
raise RuntimeError("视频生成失败:返回空文件或路径不存在")
raise ViralVideoResourceError(
resource_type="video_file", user_message="视频生成失败:返回空文件或路径不存在", detail=f"path={video_path}"
)
# #2170: 统一走方舟 Seedance(含信任链),usage 里的 provider/model_key 用于积分结算;
# 信任链产生的 Seedream 图成本已在利润率中覆盖,不单独结算。
size = Path(video_path).stat().st_size
@@ -1959,7 +2048,12 @@ def _step_upload(job: ViralVideoJob, video_path: str) -> str:
logger.info("[爆款视频] 开始上传成片: local=%s key=%s size=%d", local, storage_key, local.stat().st_size)
video_url = upload_to_oss(local, storage_key)
if not video_url:
raise RuntimeError(f"OSS 上传失败: storage_key={storage_key}")
raise ViralVideoUpstreamError(
provider="oss",
upstream_code="upload_failed",
user_message="视频上传失败,请稍后重试",
detail=f"storage_key={storage_key}",
)
return video_url
@@ -2423,7 +2517,11 @@ def _run_render_pipeline(job_id: str, session, repo, job) -> dict:
job.status,
len((job.intent_result or {}) if isinstance(job.intent_result, dict) else {}),
)
raise ValueError("文案数据缺失,请先点击「生成文案」完成文案生成后再生成视频")
raise ViralVideoError(
code="copy_missing",
user_message="文案数据缺失,请先点击「生成文案」完成文案生成后再生成视频",
http_status=400,
)
logger.info(
"[爆款视频][阶段3] 进入渲染流程 job_id=%s copy_result_shots=%d copy_result_len=%d source=%s",
job_id,
@@ -2455,12 +2553,16 @@ def _run_render_pipeline(job_id: str, session, repo, job) -> dict:
job_id,
[i.get("text", "")[:40] for i in _error_issues[:3]],
)
raise ValueError("文案合规审核未通过,请修改文案后重试或重新生成文案")
raise ViralVideoComplianceError(
user_message="文案合规审核未通过,请修改文案后重试或重新生成文案",
issues=list(_error_issues),
detail=f"{len(_error_issues)} error issues",
)
job.copy_result = copy_result
job.generated_copy_text = copy_result.get("voiceover_script", "") or ""
_save_job(repo, job, session)
except ValueError:
# #2218: 审核未通过/文案缺失的业务异常,不继续出片,向上抛出
except (ValueError, ViralVideoError):
# #2218 / PR3: 业务异常(审核未通过/文案缺失/积分不足等)不继续出片,向上抛出
raise
except Exception as e:
logger.warning("[爆款视频][阶段3] 合规审核异常,继续出片: %s", e)
@@ -0,0 +1,115 @@
"""爆款视频参数指导(persona/viral_structure/marketing_purpose/target_customer/language)。
PR3 P2(h): 把原来 worker 里 130 行 if/elif 硬编码 mapping 迁到 DB 表
`viral_video_param_guides`,提供 30 秒 TTL 缓存读取。DB miss 时返回 None,
由调用方兜底到代码默认值(保证 worker 冷启动/DB 不可用时不阻断流程)。
"""
from __future__ import annotations
import threading
import time
from typing import Optional
import sqlalchemy as sa
from packages.adapters.sqlalchemy_impl import session as _session_mod
CACHE_TTL_SECONDS = 30.0
VALID_GUIDE_TYPES = frozenset({"persona", "structure", "marketing_purpose", "target_customer", "language"})
_lock = threading.Lock()
# key = f"{guide_type}:{guide_key}", value = (expire_monotonic_ts, text|None)
_cache: dict[str, tuple[float, Optional[str]]] = {}
_lazy_session = None
def _get_session():
"""优先复用 worker 的 SessionLocal;否则懒建(API 场景)。"""
global _lazy_session
if _session_mod.SessionLocal is not None:
return _session_mod.SessionLocal()
if _lazy_session is not None:
return _lazy_session()
try:
from packages.config import get_shared_settings
url = str(get_shared_settings().database_url)
except Exception: # noqa: BLE001
return None
if not url:
return None
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg://")
if url.startswith("postgresql://") and "psycopg" not in url:
url = url.replace("postgresql://", "postgresql+psycopg://", 1)
engine = sa.create_engine(url, pool_pre_ping=True, pool_size=2, max_overflow=2)
from sqlalchemy.orm import sessionmaker
_lazy_session = sessionmaker(bind=engine)
return _lazy_session()
def _cache_key(guide_type: str, guide_key: str) -> str:
return f"{guide_type}:{guide_key}"
def _load_from_db(guide_type: str, guide_key: str) -> Optional[str]:
session = None
try:
session = _get_session()
if session is None:
return None
sql = sa.text("""
SELECT guide_text
FROM viral_video_param_guides
WHERE guide_type = :gt AND guide_key = :gk
""")
row = session.execute(sql, {"gt": guide_type, "gk": guide_key}).first()
if row is None:
return None
return row[0] if row[0] is not None else None
except Exception: # noqa: BLE001 - 表不存在/DB 不可用静默返回 None
return None
finally:
if session is not None:
try:
session.close()
except Exception: # noqa: BLE001
pass
def get_param_guide(guide_type: str, guide_key: str, *, force_refresh: bool = False) -> Optional[str]:
"""读参数指导文本,30 秒 TTL;DB 无数据返回 None,调用方自行兜底。"""
if guide_type not in VALID_GUIDE_TYPES:
return None
gk = (guide_key or "").strip()
if not gk:
return None
key = _cache_key(guide_type, gk)
now = time.monotonic()
with _lock:
if not force_refresh:
cached = _cache.get(key)
if cached and now < cached[0]:
return cached[1]
text = _load_from_db(guide_type, gk)
with _lock:
_cache[key] = (now + CACHE_TTL_SECONDS, text)
return text
def invalidate() -> None:
"""清空缓存(测试用)。"""
with _lock:
_cache.clear()
__all__ = [
"CACHE_TTL_SECONDS",
"VALID_GUIDE_TYPES",
"get_param_guide",
"invalidate",
]
@@ -1,10 +1,12 @@
"""Prompt 模板加载器:从 viral_video_prompt_templates 读模板,30 秒 TTL 热加载。
DB 不可用或没有数据时自动回落到 prompts.DEFAULT_TEMPLATES,保证流程不阻断。
DB 不可用或没有数据时自动回落到 prompts.DEFAULT_TEMPLATES,保证流程不阻断;
首次回落会打 warning(PR3 P2(f)),避免静默掩盖配置问题,又不刷屏。
"""
from __future__ import annotations
import logging
import threading
import time
from dataclasses import dataclass
@@ -15,10 +17,28 @@ import sqlalchemy as sa
from packages.adapters.sqlalchemy_impl import session as _session_mod
from packages.application.viral_video.prompts import DEFAULT_TEMPLATES
logger = logging.getLogger(__name__)
CACHE_TTL_SECONDS = 30.0
_VALID_TYPES = {"image_analysis", "storyboard", "review"}
# PR3 P2(f): 回落 warning 去重 — key -> 上次打 warning 的 monotonic 时间戳,
# 同一 key 在 _WARN_COOLDOWN_SECONDS 内不再重复告警。
_WARN_COOLDOWN_SECONDS = 300.0 # 5 分钟
_warned: dict[str, float] = {}
_warn_lock = threading.Lock()
def _warn_once(key: str, template_type: str, e: BaseException | str | None = None) -> None:
now = time.monotonic()
with _warn_lock:
last = _warned.get(key, 0.0)
if now - last < _WARN_COOLDOWN_SECONDS:
return
_warned[key] = now
logger.warning("viral_video prompt template %s fallback to default: %s", template_type, e)
@dataclass
class PromptTemplate:
@@ -64,12 +84,15 @@ def _get_session():
from packages.config import get_shared_settings
url = str(get_shared_settings().database_url)
except Exception: # noqa: BLE001
except Exception as e: # noqa: BLE001
_warn_once("session_config", "db_config", e)
return None
if not url:
_warn_once("session_url", "db_url", "database_url empty")
return None
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg://")
url = url.replace("postgresql://", "postgresql+psycopg://") if url.startswith("postgresql://") else url
if url.startswith("postgresql://") and "psycopg" not in url:
url = url.replace("postgresql://", "postgresql+psycopg://", 1)
engine = sa.create_engine(url, pool_pre_ping=True, pool_size=2, max_overflow=2)
from sqlalchemy.orm import sessionmaker
@@ -104,14 +127,15 @@ def _load_from_db(prompt_type: str) -> Optional[PromptTemplate]:
example_output=row[5] or "",
is_active=bool(row[6]),
)
except Exception: # noqa: BLE001 - 表不存在/DB 不可用时静默回落
except Exception as e: # noqa: BLE001 - 表不存在/DB 不可用时静默回落
_warn_once(f"load_db:{prompt_type}", prompt_type, e)
return None
finally:
if session is not None:
try:
session.close()
except Exception: # noqa: BLE001
pass
except Exception as e: # noqa: BLE001
_warn_once(f"session_close:{prompt_type}", prompt_type, e)
def get_template(prompt_type: str, *, force_refresh: bool = False) -> Optional[PromptTemplate]:
@@ -126,6 +150,8 @@ def get_template(prompt_type: str, *, force_refresh: bool = False) -> Optional[P
return cached[1]
template = _load_from_db(prompt_type) or _fallback(prompt_type)
if template is None:
_warn_once(f"no_template:{prompt_type}", prompt_type, "no template in db or code defaults")
if template is not None:
with _lock:
_cache[prompt_type] = (now, template)
@@ -136,6 +162,8 @@ def invalidate() -> None:
"""清空缓存(测试用)。"""
with _lock:
_cache.clear()
with _warn_lock:
_warned.clear()
class _SafeDict(dict):
@@ -146,7 +174,8 @@ class _SafeDict(dict):
def _safe_format(text: str, kwargs: dict) -> str:
try:
return text.format_map(_SafeDict(kwargs))
except Exception: # noqa: BLE001
except Exception as e: # noqa: BLE001
_warn_once("safe_format", "format", e)
return text
+88
View File
@@ -0,0 +1,88 @@
"""爆款视频领域层包(PR3 P2:从单文件转为包,保持原有对外符号不变)。"""
from packages.domain.viral_video.entities import (
STAGE_LABELS,
FusionLevel,
PromptType,
StyleStrength,
ViralVideoJob,
ViralVideoStage,
ViralVideoStatus,
)
from packages.domain.viral_video.errors import (
ViralVideoComplianceError,
ViralVideoConfigError,
ViralVideoCreditsError,
ViralVideoError,
ViralVideoLLMError,
ViralVideoResourceError,
ViralVideoTimeoutError,
ViralVideoUpstreamError,
)
from packages.domain.viral_video.stages import (
PROGRESS_COPY_DONE,
PROGRESS_COPY_START,
PROGRESS_COPY_STREAMING,
PROGRESS_DONE,
PROGRESS_IMAGE_DONE,
PROGRESS_REVIEW,
PROGRESS_START,
PROGRESS_TRUST,
PROGRESS_TTS,
PROGRESS_UPLOAD_DONE,
PROGRESS_VIDEO_DONE,
PROGRESS_VIDEO_START,
STAGE_COPY_GENERATION,
STAGE_IMAGE_ANALYSIS,
STAGE_INTENT_PARSING,
STAGE_PHASE_MESSAGES,
STAGE_REVIEW,
STAGE_TRUST_CHAIN,
STAGE_TTS,
STAGE_UPLOADING,
STAGE_VIDEO_ANALYSIS,
STAGE_VIDEO_GENERATION,
)
__all__ = [
# entities
"FusionLevel",
"PromptType",
"STAGE_LABELS",
"StyleStrength",
"ViralVideoJob",
"ViralVideoStage",
"ViralVideoStatus",
# errors
"ViralVideoError",
"ViralVideoConfigError",
"ViralVideoUpstreamError",
"ViralVideoComplianceError",
"ViralVideoResourceError",
"ViralVideoCreditsError",
"ViralVideoTimeoutError",
"ViralVideoLLMError",
# stages
"STAGE_IMAGE_ANALYSIS",
"STAGE_VIDEO_ANALYSIS",
"STAGE_COPY_GENERATION",
"STAGE_INTENT_PARSING",
"STAGE_REVIEW",
"STAGE_TRUST_CHAIN",
"STAGE_TTS",
"STAGE_VIDEO_GENERATION",
"STAGE_UPLOADING",
"STAGE_PHASE_MESSAGES",
"PROGRESS_START",
"PROGRESS_IMAGE_DONE",
"PROGRESS_COPY_START",
"PROGRESS_COPY_STREAMING",
"PROGRESS_COPY_DONE",
"PROGRESS_REVIEW",
"PROGRESS_TRUST",
"PROGRESS_TTS",
"PROGRESS_VIDEO_START",
"PROGRESS_VIDEO_DONE",
"PROGRESS_UPLOAD_DONE",
"PROGRESS_DONE",
]
+167
View File
@@ -0,0 +1,167 @@
"""爆款视频领域统一异常体系(PR3 P2)。
所有爆款视频相关业务异常均继承 ViralVideoError,保证:
1. 上层可统一捕获,按 code/user_message 组装前端响应;
2. 不暴露内部栈/traceback;
3. http_status 供 API 层映射 HTTP 状态码。
"""
from __future__ import annotations
class ViralVideoError(Exception):
"""爆款视频业务异常基类。"""
code: str = "viral_video_error"
user_message: str = "视频生成失败,请稍后重试"
detail: str | None = None
http_status: int = 500
def __init__(
self,
user_message: str | None = None,
detail: str | None = None,
code: str | None = None,
http_status: int | None = None,
) -> None:
if user_message is not None:
self.user_message = user_message
self.detail = detail
if code is not None:
self.code = code
if http_status is not None:
self.http_status = http_status
super().__init__(self.user_message)
def __str__(self) -> str: # pragma: no cover
return f"[{self.code}] {self.user_message}" + (f" :: {self.detail}" if self.detail else "")
class ViralVideoConfigError(ViralVideoError):
"""配置/初始化错误:缺模型、缺 API Key、依赖未就绪等。"""
code = "config_error"
http_status = 500
def __init__(
self,
user_message: str = "服务配置异常,请稍后重试",
detail: str | None = None,
) -> None:
super().__init__(user_message=user_message, detail=detail)
class ViralVideoUpstreamError(ViralVideoError):
"""上游模型/云服务错误(火山 Seedance、阿里 Wan、OSS 等)。"""
code = "upstream_error"
http_status = 502
def __init__(
self,
provider: str,
upstream_code: str = "unknown",
user_message: str = "视频生成服务暂时不可用,请稍后重试",
detail: str | None = None,
status_code: int | None = None,
) -> None:
self.provider = provider
self.upstream_code = upstream_code
self.status_code = status_code
super().__init__(user_message=user_message, detail=detail)
class ViralVideoComplianceError(ViralVideoError):
"""合规审核不通过(用户文案命中敏感/违规内容)。"""
code = "compliance_rejected"
http_status = 400
def __init__(
self,
user_message: str = "文案未通过合规审核,请修改后重试",
issues: list | None = None,
detail: str | None = None,
) -> None:
self.issues: list = list(issues) if issues else []
super().__init__(user_message=user_message, detail=detail)
class ViralVideoResourceError(ViralVideoError):
"""资源不存在/失效:OSS URL 过期、图片找不到、返回空文件等。"""
code = "resource_error"
http_status = 502
def __init__(
self,
resource_type: str = "file",
resource_id: str = "",
user_message: str = "素材失效,请重新上传后重试",
detail: str | None = None,
) -> None:
self.resource_type = resource_type
self.resource_id = resource_id
super().__init__(user_message=user_message, detail=detail)
class ViralVideoCreditsError(ViralVideoError):
"""积分不足/结算失败。"""
code = "credits_insufficient"
http_status = 402
def __init__(
self,
user_message: str = "积分不足,请充值后重试",
detail: str | None = None,
) -> None:
super().__init__(user_message=user_message, detail=detail)
class ViralVideoTimeoutError(ViralVideoError):
"""某阶段执行超时(LLM/渲染/上传)。"""
code = "timeout"
http_status = 504
def __init__(
self,
stage: str,
timeout_s: float = 0.0,
user_message: str = "处理超时,请稍后重试",
detail: str | None = None,
) -> None:
self.stage = stage
self.timeout_s = timeout_s
super().__init__(user_message=user_message, detail=detail)
class ViralVideoLLMError(ViralVideoError):
"""LLM 调用失败/重试耗尽/解析失败。"""
code = "llm_error"
http_status = 502
def __init__(
self,
stage: str = "script_generation",
model: str = "",
user_message: str = "文案生成失败,请稍后重试",
detail: str | None = None,
) -> None:
self.stage = stage
self.model = model
super().__init__(user_message=user_message, detail=detail)
__all__ = [
"ViralVideoError",
"ViralVideoConfigError",
"ViralVideoUpstreamError",
"ViralVideoComplianceError",
"ViralVideoResourceError",
"ViralVideoCreditsError",
"ViralVideoTimeoutError",
"ViralVideoLLMError",
]
+71
View File
@@ -0,0 +1,71 @@
"""爆款视频阶段与进度点常量(PR3 P2)。
worker/API 中硬编码的阶段字符串与进度数值集中到此文件统一维护;
注意:Celery task name / HTTP path / WS event name 属对外契约,不在此列。
"""
from __future__ import annotations
# ── 阶段标识(对齐 ViralVideoStage 但更贴合 v1.6 实际链路) ──────────────
STAGE_IMAGE_ANALYSIS: str = "image_analysis"
STAGE_VIDEO_ANALYSIS: str = "video_analysis"
STAGE_COPY_GENERATION: str = "script_generation" # v1.6 文案=编导分镜
STAGE_INTENT_PARSING: str = "intent_parsing"
STAGE_REVIEW: str = "review"
STAGE_TRUST_CHAIN: str = "trust_chain" # #2172 信任链预热
STAGE_TTS: str = "tts"
STAGE_VIDEO_GENERATION: str = "rendering"
STAGE_UPLOADING: str = "uploading"
# ── 阶段中文提示 ────────────────────────────────────────────────────────
STAGE_PHASE_MESSAGES: dict[str, str] = {
STAGE_IMAGE_ANALYSIS: "正在分析图片内容...",
STAGE_VIDEO_ANALYSIS: "正在分析参考视频风格...",
STAGE_COPY_GENERATION: "正在生成文案与分镜...",
STAGE_INTENT_PARSING: "正在解析需求...",
STAGE_REVIEW: "正在进行出片前合规审核...",
STAGE_TRUST_CHAIN: "正在AI化人像参考...",
STAGE_TTS: "正在合成AI配音...",
STAGE_VIDEO_GENERATION: "正在生成视频(约1-3分钟)...",
STAGE_UPLOADING: "正在上传成片...",
}
# ── 关键进度点(0-100)────────────────────────────────────────────────
PROGRESS_START: float = 0.0
PROGRESS_IMAGE_DONE: float = 10.0 # 图片分析完成
PROGRESS_COPY_START: float = 15.0 # 进入文案生成
PROGRESS_COPY_STREAMING: float = 30.0 # 文案流式输出中
PROGRESS_COPY_DONE: float = 45.0 # 文案生成完成
PROGRESS_REVIEW: float = 50.0 # 合规审核
PROGRESS_TRUST: float = 60.0 # 信任链 AI 化
PROGRESS_TTS: float = 65.0 # TTS 合成
PROGRESS_VIDEO_START: float = 70.0 # 开始视频生成
PROGRESS_VIDEO_DONE: float = 95.0 # 视频生成完成
PROGRESS_UPLOAD_DONE: float = 98.0 # 上传完成
PROGRESS_DONE: float = 100.0 # 全流程完成
__all__ = [
"STAGE_IMAGE_ANALYSIS",
"STAGE_VIDEO_ANALYSIS",
"STAGE_COPY_GENERATION",
"STAGE_INTENT_PARSING",
"STAGE_REVIEW",
"STAGE_TRUST_CHAIN",
"STAGE_TTS",
"STAGE_VIDEO_GENERATION",
"STAGE_UPLOADING",
"STAGE_PHASE_MESSAGES",
"PROGRESS_START",
"PROGRESS_IMAGE_DONE",
"PROGRESS_COPY_START",
"PROGRESS_COPY_STREAMING",
"PROGRESS_COPY_DONE",
"PROGRESS_REVIEW",
"PROGRESS_TRUST",
"PROGRESS_TTS",
"PROGRESS_VIDEO_START",
"PROGRESS_VIDEO_DONE",
"PROGRESS_UPLOAD_DONE",
"PROGRESS_DONE",
]
+247
View File
@@ -0,0 +1,247 @@
"""Seed viral_video_param_guides 表(PR3 P2-h)。
将 worker 中硬编码的人设/结构/语言默认值灌入 DB,保证部署后查询有默认返回;
运营可后续在后台修改,30s TTL 缓存自动生效。
用法:
cd /root/work/xiaoxia-saas
PYTHONPATH=. python scripts/seed_viral_video_param_guides.py
幂等:用 ON CONFLICT (guide_type, guide_key) DO UPDATE 保证重复执行安全。
"""
from __future__ import annotations
import sys
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
sys.path.insert(0, str(ROOT))
from sqlalchemy import create_engine, text # noqa: E402
SEED_DATA: list[tuple[str, str, str]] = [
# ── persona:人设风格 ──────────────────────────────────────────────
("persona", "宝妈", "温柔贴心,多从带娃和家庭实用角度分享,语气亲切有共鸣"),
("persona", "店主", "热情实在,像当面招呼客人,突出靠谱和实在优惠"),
("persona", "专业顾问", "专业可信,讲清原理和效果,用事实打消顾虑"),
("persona", "年轻达人", "活泼有网感,节奏轻快,金句和梗自然不尬"),
("persona", "老板型IP", "以老板第一人称出镜,真诚接地气,像招呼街坊邻居一样分享,突出创业初心和靠谱"),
("persona", "知识博主", "条理清晰、数据说话,干货密度高,语气专业但不枯燥"),
("persona", "生活美学", "画面感强,注重氛围和质感描述,语速偏慢,文字有诗意"),
("persona", "健身教练", "energetic、鼓励式口吻,强调动作要领和效果变化"),
("persona", "美妆达人", "细腻讲质地和妆效,像闺蜜安利,语气亲切有感染力"),
("persona", "美食博主", "色香味描述丰富,口语化带馋感,节奏轻快"),
("persona", "穿搭博主", "讲搭配逻辑和场景适配,时尚但不高冷,像朋友建议"),
("persona", "育儿师", "科学育儿角度,温柔坚定,给具体可操作的建议"),
# ── structure:爆款结构 ────────────────────────────────────────────
(
"structure",
"反差破局+亮明观点+还原现状",
"开头3秒用反差/痛点钩子抓注意力,中段亮出核心卖点或观点,结尾还原真实到店/使用场景引导行动",
),
(
"structure",
"痛点切入+方案展示+效果对比",
"开头直击用户痛点场景,中间展示产品/服务解决方案,结尾用前后对比强化效果",
),
(
"structure",
"故事引入+产品种草+行动引导",
"用一个真实小故事/案例引入,自然过渡到产品种草,结尾明确引导用户下一步行动",
),
(
"structure",
"场景展示+价值输出+信任背书",
"开头展示使用场景让用户代入,中间输出核心价值主张,结尾用客户评价/数据等信任背书收尾",
),
("structure", "悬念开场+层层递进+高潮转化", "开头制造悬念引发好奇,内容层层推进保持张力,高潮处给出转化钩子"),
# ── language:语言提示 ─────────────────────────────────────────────
("language", "zh-cn", "使用标准普通话,口语化表达"),
("language", "zh-tw", "使用台湾腔中文,语气温柔亲切"),
("language", "zh-hk", "使用粤语风格中文表达"),
("language", "en-us", "使用美式英语,自然口语化"),
("language", "en-gb", "使用英式英语"),
("language", "ja-jp", "使用日语,自然口语化"),
("language", "ko-kr", "使用韩语,亲切自然"),
# ── marketing_purpose:营销目的(前端枚举 + 常见关键词,20项)─────────
(
"marketing_purpose",
"获客引流",
"【口播获客】第一人称对镜头发声,突出到店/加粉福利和行动引导,节奏紧凑有钩子,结尾明确CTA",
),
(
"marketing_purpose",
"账号涨粉",
"【涨粉内容】开头有强反差/悬念钩子抓注意力,内容有信息密度和共鸣点,结尾引导关注不生硬",
),
(
"marketing_purpose",
"活动通知",
"【活动通知】开头直接说活动主题和时间,中段讲福利/优惠力度,结尾强调截止时间和参与方式,信息清晰不绕弯",
),
(
"marketing_purpose",
"口播带货",
"【口播带货】第一人称对镜头说话,像真人出镜推荐好物,有真实使用体验感,突出卖点和使用效果,结尾有购买引导",
),
(
"marketing_purpose",
"促销转化",
"【促销转化】强调限时/限量/专属优惠,制造紧迫感,价格/优惠信息清晰突出,有明确购买路径",
),
(
"marketing_purpose",
"场景种草",
"【场景种草】在真实使用场景中展示产品/服务,自然带出卖点,让用户产生代入感和'我也想试试'的欲望",
),
(
"marketing_purpose",
"痛点解决",
"【痛点解决】开头直击目标用户的具体痛点场景,中段给出解决方案和使用演示,结尾强调效果对比",
),
(
"marketing_purpose",
"功能演示",
"【功能演示】以产品功能展示为主,操作步骤清晰可见,讲清楚'怎么用'和'用了有什么效果',少废话多演示",
),
(
"marketing_purpose",
"门店发现",
"【门店探店】店主或探店博主出镜,真实展示门店环境、招牌产品/服务、价格和体验感,结尾有到店引导",
),
(
"marketing_purpose",
"到店实录",
"【到店实录】真实记录到店体验全过程,镜头跟着主走进店、看环境、体验服务、尝产品,节奏自然真实",
),
(
"marketing_purpose",
"招牌体验",
"【招牌体验】聚焦门店/品牌的招牌产品/服务,细节展示特色卖点,突出'为什么这是招牌',有记忆点",
),
(
"marketing_purpose",
"同城团购",
"【同城团购】突出本地/同城/到店属性,讲清楚团购优惠内容、使用规则、门店位置,结尾引导下单",
),
(
"marketing_purpose",
"品牌主张",
"【品牌主张】传递品牌理念和价值观,有态度有情感共鸣,画面质感高级,少硬广多故事感",
),
(
"marketing_purpose",
"品牌故事",
"【品牌故事】用叙事方式讲品牌的创立初心/工艺传承/用户故事,有情感起伏,建立品牌信任",
),
(
"marketing_purpose",
"生活方式",
"【生活方式】营造向往的生活场景和氛围,产品/服务自然融入生活片段,调性好看有质感,引发'想过这样的生活'的共鸣",
),
(
"marketing_purpose",
"创意概念",
"【创意概念】画面有创意和视觉冲击力,概念新颖不套路,适合做品牌传播内容,让人愿意转发分享",
),
(
"marketing_purpose",
"对话短剧",
"【短剧模式】必须双/多角色对话格式,台词用'角色:xxx'格式,镜头在角色间切换,有剧情冲突或反转,禁止第三人称旁白解说",
),
(
"marketing_purpose",
"反转短剧",
"【反转短剧】前半段建立常规预期,后半段出人意料反转,对话推动剧情,节奏紧凑有爽感,结尾反转干脆利落",
),
(
"marketing_purpose",
"悬念短剧",
"【悬念短剧】开头制造悬念或谜题,剧情层层推进吊足胃口,结尾揭晓答案,角色对话推进,不要旁白解说",
),
(
"marketing_purpose",
"情绪短片",
"【情绪短片】用画面、音乐和简短台词传递情绪(治愈/感动/激励/共鸣),节奏偏慢有氛围感,文字克制留白",
),
# ── target_customer:目标客群(前端枚举 8项)─────────────────────
(
"target_customer",
"小镇青年",
"【目标小镇青年】台词接地气有网感,贴近下沉市场生活场景,讲实在优惠和真实体验,节奏轻快不端着",
),
(
"target_customer",
"小镇中老年",
"【目标小镇中老年】台词慢节奏、吐字清晰、信息直白,强调实惠耐用和健康养生,少用网络梗",
),
(
"target_customer",
"Z世代",
"【目标Z世代】台词活泼有梗、节奏快,可用流行语和弹幕感表达,突出个性、有趣、社交话题性",
),
(
"target_customer",
"精致妈妈",
"【目标精致妈妈】台词温柔贴心,从育儿/家庭/品质生活角度切入,强调安全、省心、性价比,有真实带娃/家居场景共鸣",
),
(
"target_customer",
"新锐白领",
"【目标新锐白领】台词简洁干练,突出效率、品质、格调,讲职场/都市生活场景,少废话直接说价值",
),
(
"target_customer",
"资深中产",
"【目标资深中产】台词专业有深度,强调品质、品位、身份感,调性高级克制,不浮夸不喊麦",
),
(
"target_customer",
"都市蓝领",
"【目标都市蓝领】台词实在接地气,强调性价比、耐用、实惠,讲打工/租房/日常刚需场景,贴近生活",
),
(
"target_customer",
"都市银发",
"【目标都市银发】台词清晰慢节奏,强调健康、实惠、方便操作,避免复杂术语和网络用语,信息直白",
),
]
def _get_engine():
"""获取 DB engine。优先 DATABASE_URL 环境变量;否则走应用配置(与 seed_viral_video_prompts.py 一致)。"""
import os
database_url = os.environ.get("DATABASE_URL")
if database_url:
url = database_url
else:
from packages.config import get_shared_settings
url = str(get_shared_settings().database_url)
# asyncpg → psycopg2(脚本里不能用 async driver)
url = url.replace("postgresql+asyncpg://", "postgresql+psycopg2://")
if url.startswith("postgresql://") and "psycopg" not in url:
url = url.replace("postgresql://", "postgresql+psycopg2://", 1)
return create_engine(url, pool_pre_ping=True)
def main() -> None:
engine = _get_engine()
insert_sql = text("""
INSERT INTO viral_video_param_guides (guide_type, guide_key, guide_text)
VALUES (:gt, :gk, :gtxt)
ON CONFLICT (guide_type, guide_key)
DO UPDATE SET guide_text = EXCLUDED.guide_text, updated_at = NOW()
""")
count = 0
with engine.begin() as conn:
for gt, gk, gtxt in SEED_DATA:
conn.execute(insert_sql, {"gt": gt, "gk": gk, "gtxt": gtxt})
count += 1
print(f"[seed_viral_video_param_guides] upserted {count} rows")
if __name__ == "__main__":
main()
+213
View File
@@ -0,0 +1,213 @@
"""PR3 P2 爆款视频后端:errors/stages/param_guides 基础单测。"""
from __future__ import annotations
def test_errors_hierarchy():
from packages.domain.viral_video import (
ViralVideoComplianceError,
ViralVideoConfigError,
ViralVideoCreditsError,
ViralVideoError,
ViralVideoLLMError,
ViralVideoResourceError,
ViralVideoTimeoutError,
ViralVideoUpstreamError,
)
# 基类
e = ViralVideoError()
assert e.code == "viral_video_error"
assert e.http_status == 500
assert str(e)
# 上游错误
ue = ViralVideoUpstreamError("doubao", upstream_code="50411", user_message="模型不可用", status_code=400)
assert isinstance(ue, ViralVideoError)
assert ue.provider == "doubao"
assert ue.upstream_code == "50411"
assert ue.http_status == 502
assert ue.status_code == 400
# 合规错误带 issues
ce = ViralVideoComplianceError(user_message="审核未通过", issues=[{"text": "违规", "severity": "error"}])
assert ce.code == "compliance_rejected"
assert ce.http_status == 400
assert len(ce.issues) == 1
# 资源/积分/超时/LLM/配置
assert ViralVideoResourceError("video_file", "x").http_status == 502
assert ViralVideoCreditsError().http_status == 402
te = ViralVideoTimeoutError("rendering", timeout_s=180)
assert te.stage == "rendering"
assert te.http_status == 504
le = ViralVideoLLMError(stage="script", model="seed-2.1-lite", detail="max retries")
assert le.http_status == 502
assert le.model == "seed-2.1-lite"
assert ViralVideoConfigError().http_status == 500
def test_stages_constants():
from packages.domain.viral_video import (
PROGRESS_COPY_DONE,
PROGRESS_COPY_START,
PROGRESS_COPY_STREAMING,
PROGRESS_DONE,
PROGRESS_IMAGE_DONE,
PROGRESS_REVIEW,
PROGRESS_TRUST,
PROGRESS_TTS,
PROGRESS_UPLOAD_DONE,
PROGRESS_VIDEO_DONE,
PROGRESS_VIDEO_START,
STAGE_COPY_GENERATION,
STAGE_IMAGE_ANALYSIS,
STAGE_PHASE_MESSAGES,
STAGE_REVIEW,
STAGE_TRUST_CHAIN,
STAGE_TTS,
STAGE_UPLOADING,
STAGE_VIDEO_GENERATION,
)
assert STAGE_IMAGE_ANALYSIS == "image_analysis"
assert STAGE_COPY_GENERATION == "script_generation"
assert STAGE_REVIEW == "review"
assert STAGE_TRUST_CHAIN == "trust_chain"
assert STAGE_VIDEO_GENERATION == "rendering"
assert STAGE_UPLOADING == "uploading"
# 进度单调递增
assert PROGRESS_IMAGE_DONE < PROGRESS_COPY_START < PROGRESS_COPY_STREAMING < PROGRESS_COPY_DONE
assert PROGRESS_COPY_DONE < PROGRESS_REVIEW < PROGRESS_TRUST < PROGRESS_TTS < PROGRESS_VIDEO_START
assert PROGRESS_VIDEO_START < PROGRESS_VIDEO_DONE < PROGRESS_UPLOAD_DONE <= PROGRESS_DONE
# 阶段中文提示
assert "分析" in STAGE_PHASE_MESSAGES[STAGE_IMAGE_ANALYSIS]
assert "审核" in STAGE_PHASE_MESSAGES[STAGE_REVIEW]
def test_param_guides_invalid_type_returns_none():
from packages.application.viral_video.param_guides import get_param_guide
assert get_param_guide("bogus", "x") is None
assert get_param_guide("persona", "") is None
assert get_param_guide("persona", " ") is None
def test_fallback_script_honest_short(monkeypatch):
"""兜底文案必须短、诚实、基于真实 VLM 输入。"""
import sys
from unittest.mock import MagicMock
# mock celery/db 依赖
for mod in ["worker_app.celery_app", "worker_app.db"]:
monkeypatch.setitem(sys.modules, mod, MagicMock())
import celery
monkeypatch.setattr(celery, "shared_task", lambda **kw: (lambda f: f))
from worker_app.tasks import viral_video as wv
from packages.domain.viral_video import ViralVideoJob
# 有 name+brand
j = ViralVideoJob(user_id="u", duration=15, video_ratio="9:16")
j.image_analysis = {"images": [{"name": "口红", "brand": "XX牌", "type": "product"}]}
fb = wv._fallback_script(j)
assert "XX牌" in fb["voiceover_script"]
assert "口红" in fb["voiceover_script"]
assert len(fb["voiceover_script"]) < 20 # 非常短,不再是营销套话
assert fb["shots"] and len(fb["shots"]) == 1 # 只有一个镜头
assert fb["shots"][0]["time_range"] == "0-15秒"
# 只有 name
j2 = ViralVideoJob(user_id="u", duration=10)
j2.image_analysis = {"images": [{"name": "面馆", "type": "store"}]}
fb2 = wv._fallback_script(j2)
assert "来看看面馆" in fb2["voiceover_script"]
assert "都很赞" not in fb2["voiceover_script"] # 不再有营销套话
# 都没有
j3 = ViralVideoJob(user_id="u", duration=15)
j3.image_analysis = {"images": []}
fb3 = wv._fallback_script(j3)
assert "好东西" in fb3["voiceover_script"]
# user_copy_text 优先
j4 = ViralVideoJob(user_id="u", duration=15, user_copy_text="自定义文案")
j4.image_analysis = {"images": [{"name": "X", "brand": "Y"}]}
fb4 = wv._fallback_script(j4)
assert fb4["voiceover_script"] == "自定义文案"
def test_video_analysis_returns_none_on_import_error(monkeypatch):
"""ImportError 时返回 None,下游 isinstance(dict) 会跳过。"""
import sys
from unittest.mock import MagicMock
for mod in ["worker_app.celery_app", "worker_app.db"]:
monkeypatch.setitem(sys.modules, mod, MagicMock())
import celery
monkeypatch.setattr(celery, "shared_task", lambda **kw: (lambda f: f))
# 让 import viral_video.video_analyzer 抛 ImportError
import builtins
from worker_app.tasks import viral_video as wv
from packages.domain.viral_video import ViralVideoJob
real_import = builtins.__import__
def fake_import(name, *a, **kw):
if "video_analyzer" in name:
raise ImportError("no module")
return real_import(name, *a, **kw)
monkeypatch.setattr(builtins, "__import__", fake_import)
j = ViralVideoJob(user_id="u", reference_video_url="https://x/y.mp4")
res = wv._step_video_analysis(j)
assert res is None
def test_seedance_prompt_uses_helpers(monkeypatch):
"""_assemble_seedance_prompt 拆分后输出包含必要段落,且长度合理。"""
import sys
from unittest.mock import MagicMock, patch
for mod in ["worker_app.celery_app", "worker_app.db"]:
monkeypatch.setitem(sys.modules, mod, MagicMock())
import celery
monkeypatch.setattr(celery, "shared_task", lambda **kw: (lambda f: f))
from worker_app.tasks import viral_video as wv
from packages.domain.viral_video import ViralVideoJob
j = ViralVideoJob(user_id="u", duration=15, video_ratio="9:16", images=["https://x/1.jpg"])
copy_result = {
"overview": {"theme": "口红试色", "total_duration": 15, "aspect_ratio": "9:16"},
"scene_and_lighting": "暖光,室内美妆场景",
"shots": [
{
"time_range": "0-15秒",
"shot_type_angle_movement": "中景",
"voiceover": "这支口红颜色超正",
"reference_image_index": 0,
"transition": "结束",
}
],
"hard_constraints": ["无水印"],
"negative_prompts": ["字幕"],
}
with patch("packages.domain.points_rules.get_viral_video_model_config", return_value={"provider": "doubao"}):
with patch.object(wv, "_shot_visual", return_value="女生涂口红特写"):
with patch.object(wv, "_parse_shot_seconds", return_value=(0, 15)):
p = wv._assemble_seedance_prompt(copy_result, j)
assert "【视频总览】" in p
assert "【分镜脚本】" in p
assert "【硬性约束】" in p
assert "【负面提示词】" in p
assert "@图片1" in p
assert "口红试色" in p
+1 -1
View File
@@ -966,7 +966,7 @@ class TestIssue2249UserParamsFix:
def test_bug2_persona_style_guide_covers_boss_ip(self):
"""Bug2: _PERSONA_STYLE_GUIDE 必须覆盖'老板型IP'"""
from apps.worker.worker_app.tasks.viral_video import _PERSONA_STYLE_GUIDE
from apps.worker.worker_app.tasks.viral_video import _DEFAULT_PERSONA_HINTS as _PERSONA_STYLE_GUIDE
assert "老板型IP" in _PERSONA_STYLE_GUIDE
assert "老板" in _PERSONA_STYLE_GUIDE["老板型IP"] or "第一人称" in _PERSONA_STYLE_GUIDE["老板型IP"]