bd617128ce
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 (pull_request) Successful in 2s
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 (push) Successful in 2s
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 / Validate - Style (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (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 / 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 / Check push changed paths (push) Successful in 18s
CI/CD Pipeline / Frontend Lint (push) 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 / 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 / PR Build API Image (pull_request) Successful in 1m7s
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m52s
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 / 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
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m34s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m11s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m18s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 2m15s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m45s
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) Successful in 4m13s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m27s
CI/CD Pipeline / Validate - Style (push) Successful in 6m8s
CI/CD Pipeline / Integration Tests (push) Successful in 6m25s
AI Code Review / AI Code Review (pull_request) Successful in 7m23s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m51s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 3m26s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 4m13s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 4m50s
CI/CD Pipeline / Validate - Security (push) Successful in 13m4s
CI/CD Pipeline / Unit Tests (push) Successful in 15m56s
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 / CI Gate (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
320 lines
10 KiB
Python
Executable File
320 lines
10 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)
|
||
VALID_VIDEO_RESOLUTIONS = ("480p", "720p", "1080p", "普清", "高清", "超清")
|
||
|
||
|
||
# -- 编导脚本结构(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 = ""
|
||
video_resolution: str = "720p"
|
||
|
||
@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 = ""
|
||
video_resolution: str = "720p"
|
||
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 = ""
|
||
video_resolution: str = "720p"
|
||
|
||
@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
|
||
current_stage: str = (
|
||
"" # 细粒度阶段 snake_case(analyzing_images/parsing_intent/generating_script/reviewing/tts_synthesizing/rendering_video/uploading)
|
||
)
|
||
phase_message: str = "" # 中文阶段提示文案(前端轮询/SSE 直接展示)
|
||
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 = ""
|
||
video_resolution: str = "720p"
|
||
credits_prepaid: float = 0.0
|
||
credits_cost: float = 0.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
|
||
|
||
|
||
# -- 积分预估 --
|
||
|
||
|
||
class EstimateCreditsRequest(BaseModel):
|
||
"""爆款视频积分预估请求。
|
||
|
||
前端可传 model 或 video_model(兼容老字段);resolution/ratio/duration 为预估所需参数。
|
||
"""
|
||
|
||
model: str = Field(default="", alias="video_model")
|
||
resolution: str = Field(default="720p", alias="video_resolution")
|
||
ratio: str = Field(default="9:16", alias="video_ratio")
|
||
duration: int = Field(default=15, ge=5, le=30)
|
||
|
||
model_config = {"populate_by_name": True}
|
||
|
||
|
||
class CreditsFormulaBreakdown(BaseModel):
|
||
"""爆款视频积分计费公式明细(前端展示用)。"""
|
||
|
||
tokens: float = Field(..., description="估算视频 tokens 数 (duration*width*height*fps/1024)")
|
||
video_cost: float = Field(..., description="视频生成成本(元)= tokens/1e6 * model_price")
|
||
fixed_cost: float = Field(..., description="固定成本(元),含 VLM/LLM/TTS/OSS/服务器")
|
||
profit_multiplier: float = Field(..., description="利润系数(默认 1.3)")
|
||
model_price: float = Field(..., description="模型单价(元/百万 tokens)")
|
||
width: int = Field(..., description="视频宽度像素")
|
||
height: int = Field(..., description="视频高度像素")
|
||
fps: int = Field(..., description="视频帧率")
|
||
|
||
|
||
class EstimateCreditsResponse(BaseModel):
|
||
"""爆款视频积分预估响应。"""
|
||
|
||
estimated_credits: float
|
||
formula_breakdown: CreditsFormulaBreakdown = Field(..., description="计费公式明细")
|
||
|
||
|
||
class RetryViralVideoRequest(BaseModel):
|
||
"""重试爆款视频任务的请求体(可选,允许改参数重新预估积分多退少补)。
|
||
|
||
不传 body 或字段全缺省:保持原参数、不重新扣点,走默认重置+入队逻辑。
|
||
传入新的 duration/video_resolution/video_ratio/video_model:重新预估积分,
|
||
与原 credits_prepaid 比较后多退少补(差额补扣不足抛 402)。
|
||
"""
|
||
|
||
duration: int | None = Field(default=None, ge=5, le=30, description="重试时新的视频时长(秒)")
|
||
video_resolution: str | None = Field(default=None, description="重试时新的分辨率,如 720p/1080p")
|
||
video_ratio: str | None = Field(default=None, description="重试时新的画幅比,如 9:16/16:9")
|
||
video_model: str | None = Field(default=None, description="重试时新的视频模型,如 seedance-2.5")
|
||
|
||
|
||
# -- 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)
|