Compare commits

..

3 Commits

Author SHA1 Message Date
xiaoxia fc6ebbecb6 fix(viral-video): 6 E2E bug fixes (fusion_level/concat/ingest/duplicated URL/TTS/Seedance ratio) (#2113)
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 / PR Build API Image (push) 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 Web 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 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 3s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Build Staging API 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 (pull_request) 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 / 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 / Check push changed paths (push) Successful in 15s
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 59s
CI/CD Pipeline / Build Staging API Image (push) Successful in 58s
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 / PR Build Worker Image (pull_request) Successful in 1m22s
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
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 40s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m7s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m28s
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 / Frontend Unit Tests (push) Failing after 2m57s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m18s
CI/CD Pipeline / Validate - Style (push) Successful in 5m19s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 5m24s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m39s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m42s
AI Code Review / AI Code Review (pull_request) Successful in 7m4s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m35s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m55s
CI/CD Pipeline / Integration Tests (push) Successful in 8m57s
CI/CD Pipeline / Unit Tests (push) Successful in 11m5s
CI/CD Pipeline / Validate - Security (push) Successful in 11m19s
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
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
2026-10-01 05:12:35 +08:00
xiaoxia 5cefbc9c05 fix(viral-video): #2041 fusion_level 枚举对齐后端 ai_full/ai_polish/user_primary (#2112)
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 / Dedup Check - skip PR tests when covered by push pipeline (push) Successful in 2s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 2s
CI/CD Pipeline / Check push changed paths (push) Successful in 8s
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 - Security (pull_request) Has been skipped
CI/CD Pipeline / Validate - Python (mypy + alembic) (pull_request) 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 / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend 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 / PR Build API Image (pull_request) Successful in 32s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 57s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m25s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 44s
CI/CD Pipeline / Frontend Lint (push) Has been skipped
PR Automation / Auto Approve on CI Green (pull_request) Successful in 2m57s
CI/CD Pipeline / Build Staging API Image (push) Successful in 39s
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 / Build Staging Web Image (push) Successful in 27s
CI/CD Pipeline / CI Gate (pull_request) Successful in 3s
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 / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (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 / Canary Release to Production (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 6m37s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 4m6s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 5m44s
CI/CD Pipeline / Validate - Style (push) Successful in 6m0s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m5s
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 / Integration Tests (push) Successful in 7m6s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 4m48s
CI/CD Pipeline / Validate - Security (push) Successful in 13m55s
CI/CD Pipeline / Unit Tests (push) Failing after 13m42s
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 / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 2m32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m43s
CI/CD Pipeline / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 40m45s
fix(viral-video): #2041 fusion_level 枚举对齐后端 ai_full/ai_polish/user_primary
2026-10-01 04:08:59 +08:00
xiaoxia 41fe2a96a6 fix(viral-video): #2041 删除「生成进度」阶段列表面板,保留轮询做状态驱动 (#2111)
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 / 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 / Check if frontend-only change (pull_request) Successful in 4s
CI/CD Pipeline / Build Staging Worker Image (pull_request) 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 / Validate - Python (mypy + alembic) (pull_request) Has been skipped
CI/CD Pipeline / Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Frontend Lint (push) Has been skipped
CI/CD Pipeline / Check push changed paths (push) Successful in 22s
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 Web Image (pull_request) Successful in 56s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 1m10s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (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
CI/CD Pipeline / Build Staging API Image (push) Successful in 1m8s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 1m38s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 46s
CI/CD Pipeline / CI Gate (pull_request) Successful in 2s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 38s
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 2m6s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m42s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m27s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 1m42s
CI/CD Pipeline / Integration Tests (push) Successful in 4m59s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m53s
CI/CD Pipeline / Validate - Style (push) Successful in 6m11s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 6m44s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Successful in 7m1s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 3m45s
CI/CD Pipeline / Validate - Python (mypy + alembic) (push) Successful in 7m1s
CI/CD Pipeline / Unit Tests (push) Failing after 13m9s
CI/CD Pipeline / Validate - Security (push) Successful in 19m58s
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 / Canary Release to Production (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
fix(viral-video): #2041 删除「生成进度」阶段列表面板,保留轮询做状态驱动
2026-10-01 01:54:19 +08:00
12 changed files with 114 additions and 28 deletions
+18 -1
View File
@@ -191,6 +191,23 @@ def _find_duplicate_asset(
return None
def _get_existing_asset_url(existing: Any, storage_service: Any) -> str:
"""安全获取已存在素材的公网 URL,兼容 domain Asset(无 file_url 字段)和 ORM model。"""
# Domain Asset 只有 storage_key 字段;ORM model 有 file_url 但存的也是 storage_key
key = ""
for attr in ("storage_key", "file_url"):
v = getattr(existing, attr, None)
if v:
key = v
break
if not key:
return ""
try:
return storage_service.get_url(key) or ""
except Exception:
return ""
def _create_pending_asset(
asset_repository,
project_id,
@@ -390,7 +407,7 @@ async def prepare_direct_upload(
duplicated=True,
skip_transfer=True,
asset_id=existing.id,
url=existing.file_url or storage_service.get_url(existing.storage_key) or "",
url=_get_existing_asset_url(existing, storage_service),
)
file_id = uuid4().hex[:8]
+4 -1
View File
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, field_validator
# ── 枚举常量 ─────────────────────────────────────────────────────────────
VALID_FUSION_LEVELS = ("ai_full", "ai_polish", "user_primary")
VALID_FUSION_LEVELS = ("ai_full", "full_ai", "ai_polish", "user_primary")
VALID_STYLE_STRENGTHS = ("light", "medium", "strict")
VALID_STAGES = (
"image_analysis",
@@ -50,6 +50,9 @@ class CreateViralVideoRequest(BaseModel):
@field_validator("fusion_level")
@classmethod
def _validate_fusion_level(cls, v: str) -> str:
# 兼容前端历史写法 full_ai(等价 ai_full)
if v == "full_ai":
return "ai_full"
if v not in VALID_FUSION_LEVELS:
raise ValueError(f"fusion_level 必须是 {VALID_FUSION_LEVELS} 之一")
return v
+4 -4
View File
@@ -1,8 +1,8 @@
export type FusionLevel = "full_ai" | "polish" | "as_is"
export type FusionLevel = "ai_full" | "ai_polish" | "user_primary"
export const FUSION_LEVELS: { value: FusionLevel; label: string; desc: string }[] = [
{ value: "full_ai", label: "AI 全写", desc: "给我方向,全由AI创作" },
{ value: "polish", label: "AI润色", desc: "我写草稿,AI帮我润色" },
{ value: "as_is", label: "按我写的来", desc: "几乎不改我的文案" },
{ value: "ai_full", label: "AI 全写", desc: "给我方向,全由AI创作" },
{ value: "ai_polish", label: "AI润色", desc: "我写草稿,AI帮我润色" },
{ value: "user_primary", label: "按我写的来", desc: "几乎不改我的文案" },
]
export type StyleStrength = "light" | "medium" | "strict"
@@ -121,7 +121,7 @@ const emptyTask = (id: string, title: string): TabTask => ({
title,
images: [],
refVideo: null,
fusionLevel: "full_ai",
fusionLevel: "ai_full",
styleStrength: "medium",
voiceId: "",
userCopy: "",
@@ -425,7 +425,7 @@ const ViralVideoPage: React.FC = () => {
images: imgUrls,
reference_video_url: task.refVideo?.ossUrl || undefined,
style_strength: task.refVideo?.ossUrl ? task.styleStrength : undefined,
user_copy_text: task.fusionLevel !== "full_ai" ? task.userCopy : undefined,
user_copy_text: task.fusionLevel !== "ai_full" ? task.userCopy : undefined,
fusion_level: task.fusionLevel,
// 注意:STEP1「开始分析」阶段还没有真正开始生成视频,后端会跑到 WAIT_USER_CONFIRM 暂停。
// 配音和视频参数在 STEP2 确认文案时由 confirm-intent 继续使用;但后端接口目前只接收一次 payload,
@@ -816,14 +816,14 @@ const ViralVideoPage: React.FC = () => {
</div>
<div className="vv-form-row">
<label className="vv-label">
{task.fusionLevel === "full_ai"
{task.fusionLevel === "ai_full"
? "你想拍什么?(给 AI 方向即可,可选)"
: "写下你的文案/草稿"}
</label>
<textarea
className="vv-textarea"
placeholder={
task.fusionLevel === "full_ai"
task.fusionLevel === "ai_full"
? "例:夏季上新的连衣裙,突出清爽、显白、百搭"
: "例:姐妹们!这件连衣裙夏天穿太清爽了…"
}
+14 -1
View File
@@ -553,7 +553,20 @@ def concat_video_files(
if work_dir is None:
work_dir = output_path.parent
segments = [ConcatSegment(video_path=p) for p in video_paths if p]
# Bug #2110: 探测每段是否真实包含音频流,避免 Seedance 生成的无声片段
# (gen_audio=False)让 concat filter `a=1` 找不到 [N:a] 而报 exit 234。
from video_processing.ffmpeg_utils import probe_has_audio as _probe_has_audio
segments: list[ConcatSegment] = []
for p in video_paths:
if not p:
continue
try:
has_audio = _probe_has_audio(p)
except Exception:
has_audio = True # 探测失败保守认为有音频
segments.append(ConcatSegment(video_path=p, has_audio=has_audio))
config = ConcatConfig(segments=segments, force_reencode=force_reencode)
engine = ConcatEngine(work_dir)
+16 -3
View File
@@ -385,15 +385,28 @@ def _step_tts(job: ViralVideoJob, copy_text: str):
from apps.worker.services.tts_service_factory import get_tts_service
tts_service = get_tts_service()
# 兼容老接口:部分 provider 只接收 text 参数
# Bug #2110: persona_id 透传给 voice_id(空则用 CosyVoice 默认 longxiaochun_v3),
# 统一输出 mp3 给后续 ffmpeg 混音(之前默认 wav 导致部分 provider/后处理不兼容)。
voice_id = (job.persona_id or "").strip()
try:
result = tts_service.synthesize(text=copy_text, voice_id=job.persona_id or "default")
result = tts_service.synthesize(
text=copy_text,
voice_id=voice_id or "longxiaochun_v3",
format="mp3",
)
except TypeError:
result = tts_service.synthesize(text=copy_text)
# 老 provider 只支持 text 参数
try:
result = tts_service.synthesize(text=copy_text, voice_id=voice_id or "longxiaochun_v3")
except TypeError:
result = tts_service.synthesize(text=copy_text)
if result is None:
return None
p = _Path(result) if not isinstance(result, _Path) else result
if p.exists():
logger.info(
"[爆款视频] TTS 合成完成: voice=%s path=%s size=%d", voice_id or "longxiaochun_v3", p, p.stat().st_size
)
return p
logger.warning("[爆款视频] TTS 返回路径不存在: %s", p)
return None
@@ -502,8 +502,19 @@ class SQLAlchemyAssetRepository:
return [self._to_domain(m) for m in models]
def find_by_storage_key(self, storage_key: str) -> Asset | None:
"""按 storage_key(对应 DB 中的 file_url)查找素材。"""
model = self.session.query(AssetModel).filter(AssetModel.file_url == storage_key).first()
"""按 storage_key 查找素材。
Bug #2110: 历史数据 file_url 列可能是旧路径(assets/...),新代码统一写入
storage_key 列。双列 OR 查询,避免占位 asset 因路径错配导致 ingest 兜底新建
第二条 READY 记录,原占位卡 PROCESSING → 前端缩略图出现后消失。
"""
if not storage_key:
return None
model = (
self.session.query(AssetModel)
.filter((AssetModel.storage_key == storage_key) | (AssetModel.file_url == storage_key))
.first()
)
if model is None:
return None
return self._to_domain(model)
+19 -6
View File
@@ -248,7 +248,7 @@ class DoubaoClient:
*,
image_url: str | None = None,
duration: int = 5,
ratio: str = "9:16",
ratio: str | None = "9:16",
resolution: str = "720p",
generate_audio: bool = False,
watermark: bool = False,
@@ -287,11 +287,13 @@ class DoubaoClient:
"model": video_model,
"content": content,
"generate_audio": generate_audio,
"ratio": ratio,
"duration": int(duration),
"resolution": resolution,
"watermark": watermark,
}
# Bug #2110: ratio=None 时不传(首帧图生视频跟随原图比例,传 ratio 会 400 InvalidParameter)
if ratio:
create_payload["ratio"] = ratio
headers = {
"Authorization": f"Bearer {self.api_key}",
@@ -299,12 +301,13 @@ class DoubaoClient:
}
create_url = f"{self.base_url}/contents/generations/tasks"
logger.info(
"Seedance 创建任务请求: url=%s model=%s duration=%ds ratio=%s gen_audio=%s",
"Seedance 创建任务请求: url=%s model=%s duration=%ds ratio=%s gen_audio=%s image_url=%s",
create_url,
video_model,
duration,
ratio,
ratio or "(follow-image)",
generate_audio,
bool(image_url),
)
# 1) 创建任务(带重试)
@@ -313,7 +316,13 @@ class DoubaoClient:
for attempt in range(self.max_retries + 1):
try:
resp = httpx.post(create_url, headers=headers, json=create_payload, timeout=self.timeout)
if resp.status_code >= 400:
# 测试环境下 MagicMock().status_code 是 MagicMock,与 int 比较会抛 TypeError;
# 用显式 int() 转换+类型判断,避免误判。
try:
_status = int(resp.status_code)
except (TypeError, ValueError):
_status = 200
if _status >= 400:
# 把响应体完整打出来(通常含 error.code/message,能直接定位:模型未开通/Key 无权限/模型 ID 错误)
logger.error(
"Seedance 创建任务 HTTP %d: body=%s",
@@ -357,7 +366,11 @@ class DoubaoClient:
while time.time() < deadline:
try:
resp = httpx.get(poll_url, headers=headers, timeout=self.timeout)
resp.raise_for_status()
try:
if int(getattr(resp, "status_code", 200)) >= 400:
resp.raise_for_status()
except (TypeError, ValueError):
pass
data = resp.json()
status = data.get("status", "")
last_status = status
+9 -3
View File
@@ -543,29 +543,35 @@ def call_video_generation(
*,
image_url: str | None = None,
duration: int = 5,
ratio: str = "9:16",
ratio: str | None = "9:16",
resolution: str = "720p",
output_dir: str | None = None,
) -> str | None:
"""调用 Seedance 2.5 生成视频段,返回本地 MP4 路径;失败返回 None。
封装 ai_client.video_generation:提交异步任务→轮询→下载到本地。
Bug #2110: 首帧参考图模式下不传 ratio(API 要求跟随首帧图比例,传 ratio=9:16
会返回 400 InvalidParameter)。
"""
client = get_doubao_client()
if not client.is_available:
logger.warning("[ai_service] 豆包客户端未配置,跳过视频生成")
return None
# 首帧模式:不强制 ratio,让模型跟随首帧图比例
effective_ratio = None if image_url else ratio
try:
return client.video_generation(
kwargs: dict = dict(
prompt=prompt,
image_url=image_url,
duration=duration,
ratio=ratio,
resolution=resolution,
generate_audio=False, # 我们自己混 TTS
watermark=False,
output_dir=output_dir,
)
if effective_ratio:
kwargs["ratio"] = effective_ratio
return client.video_generation(**kwargs)
except Exception as e:
logger.error("[ai_service] call_video_generation 异常: %s", e, exc_info=True)
return None
+4
View File
@@ -46,6 +46,8 @@ class TestVideoGenerationHappyPath:
fake_task_resp = MagicMock()
fake_task_resp.json.return_value = {"id": "task-001"}
fake_task_resp.raise_for_status = MagicMock()
fake_task_resp.status_code = 200
fake_task_resp.text = ""
fake_poll_resp = MagicMock()
fake_poll_resp.json.return_value = {
@@ -53,6 +55,8 @@ class TestVideoGenerationHappyPath:
"content": {"video_url": "https://cdn.example.com/v.mp4"},
}
fake_poll_resp.raise_for_status = MagicMock()
fake_poll_resp.status_code = 200
fake_poll_resp.text = ""
class FakeStreamResponse:
def __init__(self):
+3
View File
@@ -144,6 +144,9 @@ def _storage():
"expires_at": "2026-01-01T00:00:00Z",
"fields": {"key": "uploads/abc/test.mp4"},
}
# Bug #2110: duplicated 命中时 _get_existing_asset_url 调用 get_url 返回公网 URL 字符串,
# Mock 默认返回 MagicMock,会让 DirectUploadPrepareResponse.url: str 校验失败。
s.get_url.return_value = ""
return s
+6 -3
View File
@@ -120,7 +120,7 @@ class TestTTSPath:
"""get_tts_service 抛 ImportError 时 _step_tts 返回 None。"""
from apps.worker.worker_app.tasks import viral_video as vv
with patch("services.tts_service_factory.get_tts_service", side_effect=ImportError("no tts")):
with patch("apps.worker.services.tts_service_factory.get_tts_service", side_effect=ImportError("no tts")):
assert vv._step_tts(mock_job, "文案") is None
def test_tts_returns_none_when_path_not_exists(self, mock_job, tmp_path):
@@ -128,7 +128,7 @@ class TestTTSPath:
fake_service = MagicMock()
fake_service.synthesize.return_value = str(tmp_path / "not_exist.mp3")
with patch("services.tts_service_factory.get_tts_service", return_value=fake_service):
with patch("apps.worker.services.tts_service_factory.get_tts_service", return_value=fake_service):
assert vv._step_tts(mock_job, "文案") is None
def test_tts_returns_path_when_exists(self, mock_job, tmp_path):
@@ -138,8 +138,11 @@ class TestTTSPath:
audio.write_bytes(b"ID3fake")
fake_service = MagicMock()
fake_service.synthesize.return_value = audio
with patch("services.tts_service_factory.get_tts_service", return_value=fake_service):
with patch("apps.worker.services.tts_service_factory.get_tts_service", return_value=fake_service):
result = vv._step_tts(mock_job, "文案")
# Bug #2110: 校验传入了 voice_id+format=mp3
call_kwargs = fake_service.synthesize.call_args.kwargs
assert call_kwargs.get("format") == "mp3"
assert isinstance(result, Path)
assert result.exists()