649420bd35
CI/CD Pipeline / Check push changed paths (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 1s
CI/CD Pipeline / Dedup Check - skip PR tests when covered by push pipeline (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / 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 1m12s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m21s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 1m29s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 2m0s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Successful in 2m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m18s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m13s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m42s
AI Code Review / AI Code Review (pull_request) Successful in 6m41s
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Style (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Security (pull_request) Has been cancelled
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been cancelled
- 输出从营销口播文案改为专业编导分镜脚本(CopyResult v1.6:overview/scene_and_lighting/shots/hard_constraints/negative_prompts/voiceover_script) - 新 SCRIPT_GENERATION 阶段替代原 copy_fusion+storyboard+review,prompt 指导 LLM 输出严格 JSON 结构 - pipeline 简化为4步:图片分析 → 编导脚本 → TTS整段合成(上传OSS做reference_audios) → 单次Seedance出片 → 上传 - 删除:多段分镜拆分、ffmpeg concat拼接、placeholder占位视频、分段重试降级、BGM单独混音(Seedance generate_audio=true原生合成音效/BGM) - ai_client/ai_service video_generation 支持 reference_images/reference_audios/reference_videos/generate_audio 参数 - duration 默认15秒,上限30秒;前端时长下拉 5/10/15/20/25/30s - 首帧图模式不传 ratio(保持 #2110 修复) - Alembic migration 088 幂等添加 voice_id/voice_source/video_ratio/video_model/copy_result 五列 - _step_image_analysis call_vision() 全路径 None 防护 - 向后兼容:final_copy=voiceover_script、storyboard=shots、老数据 _build_copy_result 降级拼装 - TS types 更新 CopyResult v1.6 结构 + ShotScript/Overview - 96个 viral 相关单测全通过
259 lines
7.6 KiB
Python
Executable File
259 lines
7.6 KiB
Python
Executable File
"""爆款视频 API schemas (v1.6 单次 Seedance 出片版)。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import datetime
|
||
|
||
from pydantic import BaseModel, Field, field_validator
|
||
|
||
# -- 枚举常量 --
|
||
|
||
VALID_FUSION_LEVELS = ("ai_full", "full_ai", "ai_polish", "user_primary")
|
||
VALID_STYLE_STRENGTHS = ("light", "medium", "strict")
|
||
VALID_STAGES = (
|
||
"image_analysis",
|
||
"video_analysis",
|
||
"intent_parsing",
|
||
"script_generation",
|
||
"review",
|
||
"tts",
|
||
"rendering",
|
||
"uploading",
|
||
)
|
||
VALID_VIDEO_RATIOS = ("9:16", "16:9", "1:1", "4:3", "3:4", "21:9")
|
||
VALID_DURATIONS = (5, 10, 15, 20, 25, 30)
|
||
|
||
|
||
# -- 编导脚本结构(v1.6) --
|
||
|
||
|
||
class ShotScript(BaseModel):
|
||
"""逐镜头分镜。"""
|
||
|
||
time_range: str = Field(default="", description="时间区间,如 0-3秒")
|
||
shot_type_angle_movement: str = Field(default="", description="景别/角度/运镜,如『近景俯拍45度,缓慢推镜』")
|
||
scene_and_dialogue: str = Field(default="", description="场景描述+口播台词")
|
||
action_details: str = Field(default="", description="人物动作、表情、物品操作细节")
|
||
audio_bgm: str = Field(default="", description="环境音+BGM提示")
|
||
transition: str = Field(default="硬切", description="转场方式:硬切/淡入淡出/叠化")
|
||
reference_image_index: int | None = Field(
|
||
default=None, description="参考图片索引(0-based,对应上传的第几张产品图)"
|
||
)
|
||
|
||
|
||
class CopyResultOverview(BaseModel):
|
||
theme: str = ""
|
||
total_duration: int = 15
|
||
aspect_ratio: str = "9:16"
|
||
|
||
|
||
class CopyResult(BaseModel):
|
||
"""v1.6 编导分镜脚本结构(给前端 + Seedance 用)。"""
|
||
|
||
overview: CopyResultOverview = Field(default_factory=CopyResultOverview)
|
||
scene_and_lighting: str = ""
|
||
shots: list[ShotScript] = Field(default_factory=list)
|
||
hard_constraints: list[str] = Field(default_factory=list)
|
||
negative_prompts: list[str] = Field(default_factory=list)
|
||
voiceover_script: str = Field(
|
||
default="", description="纯口播对白,从各镜 scene_and_dialogue 的对白部分拼接,供 TTS 使用"
|
||
)
|
||
# 向后兼容:final_copy = voiceover_script
|
||
final_copy: str = ""
|
||
suggested_copy: str = ""
|
||
title: str = ""
|
||
|
||
|
||
# -- Request Schemas --
|
||
|
||
|
||
class CreateViralVideoRequest(BaseModel):
|
||
"""旧接口:一键创建(保留兼容)。"""
|
||
|
||
images: list[str] = Field(..., min_length=1, max_length=20)
|
||
industry: str = ""
|
||
target_customer: str = ""
|
||
persona_id: str = ""
|
||
viral_structure: str = ""
|
||
marketing_purpose: str = ""
|
||
bgm_preference: str = ""
|
||
duration: int = Field(default=15, ge=5, le=30, description="视频时长(秒),5-30")
|
||
user_copy_text: str = ""
|
||
fusion_level: str = "ai_polish"
|
||
reference_audio_path: str = ""
|
||
reference_video_url: str = ""
|
||
style_strength: str = "medium"
|
||
style_template_id: str = ""
|
||
voice_id: str = ""
|
||
voice_source: str = ""
|
||
video_ratio: str = "9:16"
|
||
video_model: str = ""
|
||
|
||
@field_validator("fusion_level")
|
||
@classmethod
|
||
def _v_fl(cls, v: str) -> str:
|
||
if v == "full_ai":
|
||
return "ai_full"
|
||
if v not in VALID_FUSION_LEVELS:
|
||
raise ValueError(f"fusion_level must be one of {VALID_FUSION_LEVELS}")
|
||
return v
|
||
|
||
@field_validator("style_strength")
|
||
@classmethod
|
||
def _v_ss(cls, v: str) -> str:
|
||
if v not in VALID_STYLE_STRENGTHS:
|
||
raise ValueError(f"style_strength must be one of {VALID_STYLE_STRENGTHS}")
|
||
return v
|
||
|
||
|
||
class AnalyzeImagesRequest(BaseModel):
|
||
"""v1.5+ 阶段1:创建任务 + 图片/视频分析。"""
|
||
|
||
images: list[str] = Field(..., min_length=1, max_length=30)
|
||
reference_video_url: str = ""
|
||
style_template_id: str = ""
|
||
style_strength: str = "medium"
|
||
voice_id: str = ""
|
||
voice_source: str = ""
|
||
video_ratio: str = "9:16"
|
||
video_model: str = ""
|
||
duration: int = Field(default=15, ge=5, le=30)
|
||
|
||
|
||
class GenerateCopyRequest(BaseModel):
|
||
"""v1.5+ 阶段2:填完营销参数,生成编导脚本。"""
|
||
|
||
industry: str = ""
|
||
target_customer: str = ""
|
||
persona_id: str = ""
|
||
viral_structure: str = ""
|
||
marketing_purpose: str = ""
|
||
bgm_preference: str = ""
|
||
duration: int = Field(default=15, ge=5, le=30)
|
||
user_copy_text: str = ""
|
||
fusion_level: str = "ai_polish"
|
||
reference_audio_path: str = ""
|
||
reference_video_url: str = ""
|
||
style_strength: str = "medium"
|
||
style_template_id: str = ""
|
||
style_guide: dict | None = None
|
||
voice_id: str = ""
|
||
voice_source: str = ""
|
||
video_ratio: str = "9:16"
|
||
video_model: str = ""
|
||
|
||
@field_validator("fusion_level")
|
||
@classmethod
|
||
def _v_fl(cls, v: str) -> str:
|
||
if v == "full_ai":
|
||
return "ai_full"
|
||
if v not in VALID_FUSION_LEVELS:
|
||
raise ValueError(f"fusion_level must be one of {VALID_FUSION_LEVELS}")
|
||
return v
|
||
|
||
@field_validator("style_strength")
|
||
@classmethod
|
||
def _v_ss(cls, v: str) -> str:
|
||
if v not in VALID_STYLE_STRENGTHS:
|
||
raise ValueError(f"style_strength must be one of {VALID_STYLE_STRENGTHS}")
|
||
return v
|
||
|
||
|
||
class ConfirmCopyRequest(BaseModel):
|
||
"""v1.5+ 阶段3:用户确认/编辑口播后开始渲染(TTS+单次Seedance)。"""
|
||
|
||
edited_copy: str = Field(default="", description="用户编辑后的口播文案;为空则用 AI 生成的 voiceover_script")
|
||
|
||
|
||
class ConfirmIntentRequest(BaseModel):
|
||
"""旧 confirm-intent(兼容)。"""
|
||
|
||
confirmed_copy: str = ""
|
||
adjustments: str = ""
|
||
|
||
|
||
class AnalyzeStyleRequest(BaseModel):
|
||
reference_video_url: str = Field(..., description="参考视频 URL")
|
||
style_template_id: str = ""
|
||
|
||
|
||
# -- Response Schemas --
|
||
|
||
|
||
class ViralVideoJobResponse(BaseModel):
|
||
"""爆款视频任务响应(v1.6 包含 copy_result 编导脚本结构)。"""
|
||
|
||
id: str
|
||
user_id: str
|
||
images: list[str] = Field(default_factory=list)
|
||
industry: str = ""
|
||
target_customer: str = ""
|
||
persona_id: str = ""
|
||
viral_structure: str = ""
|
||
marketing_purpose: str = ""
|
||
bgm_preference: str = ""
|
||
duration: int = 15
|
||
user_copy_text: str = ""
|
||
fusion_level: str = "ai_polish"
|
||
reference_audio_path: str = ""
|
||
reference_video_url: str = ""
|
||
style_strength: str = "medium"
|
||
style_guide: dict | None = None
|
||
style_template_id: str = ""
|
||
status: str
|
||
image_analysis: dict | None = None
|
||
# v1.6 编导脚本(推荐前端使用)
|
||
copy_result: dict | None = None
|
||
# v1.5 兼容字段
|
||
storyboard: list | None = None
|
||
generated_copy_text: str = ""
|
||
# 音色/视频参数
|
||
voice_id: str = ""
|
||
voice_source: str = ""
|
||
video_ratio: str = "9:16"
|
||
video_model: str = ""
|
||
intent_result: dict | None = None
|
||
result_video_url: str = ""
|
||
credits_cost: int = 0
|
||
error_msg: str = ""
|
||
retry_count: int = 0
|
||
started_at: datetime | None = None
|
||
completed_at: datetime | None = None
|
||
created_at: datetime | None = None
|
||
updated_at: datetime | None = None
|
||
|
||
|
||
class ViralVideoHistoryResponse(BaseModel):
|
||
items: list[ViralVideoJobResponse]
|
||
total: int
|
||
|
||
|
||
class StyleTemplateResponse(BaseModel):
|
||
id: str
|
||
name: str
|
||
description: str = ""
|
||
thumbnail_url: str = ""
|
||
style_config: dict = Field(default_factory=dict)
|
||
|
||
|
||
class StyleTemplateListResponse(BaseModel):
|
||
items: list[StyleTemplateResponse]
|
||
|
||
|
||
class AnalyzeStyleResponse(BaseModel):
|
||
job_id: str
|
||
status: str
|
||
style_guide: dict | None = None
|
||
|
||
|
||
# -- WebSocket 事件 Schema --
|
||
|
||
|
||
class WSProgressEvent(BaseModel):
|
||
type: str = "viral_video:progress"
|
||
job_id: str
|
||
stage: str
|
||
progress: float = Field(ge=0.0, le=100.0)
|
||
message: str = ""
|
||
data: dict = Field(default_factory=dict)
|