fix(viral-video): fix 6 E2E bugs — fusion_level alias, concat silent segments, ingest lookup, duplicated URL, TTS voice/format, Seedance first-frame ratio
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 4s
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 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 API Image (pull_request) Successful in 35s
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 53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m46s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m52s
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 / 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 / Unit Tests (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
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 4s
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 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 API Image (pull_request) Successful in 35s
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 53s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m33s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m46s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 4m52s
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 / 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 / Unit Tests (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
Bug1 (P0): schema accepts 'full_ai' as alias for 'ai_full' (Pydantic field_validator normalizes)
Bug2 (P0): concat_video_files probes each segment audio stream; Seedance gen_audio=False
segments now marked has_audio=False so concat filter uses aevalsrc silence instead
of failing with ffmpeg exit 234
Bug3 (P1): find_by_storage_key now queries (storage_key OR file_url) to cover historical
data where the legacy file_url column held assets/<project>/<date>/... paths
Bug4 (P1): duplicated-hit response no longer accesses non-existent domain Asset.file_url;
new helper _get_existing_asset_url uses storage_key (fallback file_url) through
storage_service.get_url()
Bug5 (P1): _step_tts passes job.persona_id as voice_id (default longxiaochun_v3) and
forces format='mp3' so downstream ffmpeg -map 1:a:0 works regardless of provider
Bug6 (P1): call_video_generation omits ratio param in first-frame (image_url) mode;
ai_client.video_generation ratio becomes Optional[str] and is omitted from payload
when None, fixing 400 InvalidParameter from Seedance
This commit is contained in:
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -385,15 +385,26 @@ 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)
|
||||
|
||||
@@ -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) 创建任务(带重试)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user