refactor: 统一封面生成管道 — 从渲染后视频抽帧作为封面 (#1371)
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 1m32s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m45s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 2m20s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 2m22s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 5m2s
CI/CD Pipeline / Validate - Code Quality (push) Successful in 6m19s
CI/CD Pipeline / Integration Tests (push) Successful in 2m0s
CI/CD Pipeline / Unit Tests (push) Successful in 9m9s
CI/CD Pipeline / Build Staging API Image (push) Successful in 20m32s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m2s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 41s
CI/CD Pipeline / Staging E2E Tests (push) Successful in 1m53s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 2m50s
CI/CD Pipeline / Canary Release to Production (push) Failing after 784h46m17s
CI/CD Pipeline / Deploy Production (push) Failing after 785h1m33s
CI/CD Pipeline / CI Gate (push) Failing after 785h1m33s
CI/CD Pipeline / Build Production Web Image (push) Failing after 785h1m34s
CI/CD Pipeline / Build Production API Image (push) Failing after 785h1m34s
CI/CD Pipeline / PR Build Web Image (push) Failing after 785h10m41s
CI/CD Pipeline / PR Build API Image (push) Failing after 785h10m42s
CI/CD Pipeline / Check if frontend-only change (push) Failing after 785h10m44s
CI/CD Pipeline / Production Browser E2E (push) Failing after 785h35m3s
CI/CD Pipeline / Build Production Worker Image (push) Failing after 785h35m5s
CI/CD Pipeline / PR Build Worker Image (push) Failing after 785h44m11s
CI/CD Pipeline / Frontend Lint (push) Failing after 785h44m14s

Co-authored-by: xiaoxia <dev@xiaoxiajianji.com>
Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
This commit was merged in pull request #1371.
This commit is contained in:
2026-08-14 22:16:00 +08:00
committed by auto-approve-bot
parent 85bfe58f39
commit 0301370dd8
15 changed files with 461 additions and 1152 deletions
+22 -34
View File
@@ -81,9 +81,7 @@ class RenderAdapterResult:
failed_clip_ids: list[str] = None # 失败的 clip id 列表
error_message: str = ""
error_detail: str = "" # 详细错误信息(如 ffmpeg stderr),用于排查
cover_candidates: list[dict] | None = (
None # 封面候选帧 [{"image_url": "...", "frame_time": 5.0, "storage_key": "..."}]
)
cover_url: str = "" # 封面图片 URL(从渲染后视频抽帧,天然带标题)
def __post_init__(self):
if self.rendered_clip_ids is None:
@@ -562,43 +560,33 @@ class RenderAdapter:
storage_key = f"rendered/{plan_id}/{job_id or plan_id}.mp4"
output_url = upload_to_oss(result.output_path, storage_key)
self._report_progress(progress_cb, 90.0, "生成封面缩略图")
self._report_progress(progress_cb, 90.0, "抽取封面帧")
# 6. 生成封面缩略图
thumbnail_url = ""
# 6. 从已渲染视频抽取封面帧(标题已通过 ASS 字幕烧录,封面天然带标题)
cover_url = ""
cover_frame_path = None
try:
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
from video_processing.thumbnail_generator import extract_first_frame
thumb_storage_key = f"rendered/{plan_id}/thumbnail.jpg"
thumbnail_url = generate_and_upload_thumbnail(str(result.output_path), thumb_storage_key)
except Exception as thumb_err:
logger.warning(
"[render-adapter] 缩略图生成失败(不影响主流程): plan_id=%s error=%s",
plan_id,
thumb_err,
)
# 7. 抽取封面候选帧并上传 OSS(失败不阻断主流程)
cover_candidates = None
try:
from video_processing.thumbnail_generator import extract_and_upload_cover_frames
# 从 plan config 提取标题文字,叠加到封面候选帧上
_title_cfg = (plan_config or {}).get("title", {}) or {}
_title_text = (_title_cfg.get("text", "") or "").strip() if _title_cfg.get("enabled", True) else ""
cover_candidates = extract_and_upload_cover_frames(
str(result.output_path), plan_id, num_frames=3, title_text=_title_text
)
if cover_candidates:
cover_frame_path = extract_first_frame(str(result.output_path), width=640)
cover_storage_key = f"rendered/{plan_id}/cover.jpg"
try:
cover_url = upload_to_oss(cover_frame_path, cover_storage_key) or ""
finally:
if cover_frame_path:
try:
Path(cover_frame_path).unlink(missing_ok=True)
except Exception:
pass
if cover_url:
logger.info(
"[render-adapter] 封面候选帧生成成功: plan_id=%s count=%d",
"[render-adapter] 封面帧提取成功: plan_id=%s url=%s",
plan_id,
len(cover_candidates),
cover_url[:80],
)
except Exception as cover_err:
logger.warning(
"[render-adapter] 封面候选帧生成失败(不影响主流程): plan_id=%s error=%s",
"[render-adapter] 封面帧提取失败(不影响主流程): plan_id=%s error=%s",
plan_id,
cover_err,
)
@@ -626,7 +614,7 @@ class RenderAdapter:
success=True,
output_url=output_url or "",
output_path=result.output_path,
thumbnail_url=thumbnail_url,
thumbnail_url=cover_url,
duration=result.duration,
file_size=result.file_size,
width=result.width,
@@ -634,7 +622,7 @@ class RenderAdapter:
clip_count=len(clips),
rendered_clip_ids=final_rendered_ids,
failed_clip_ids=final_failed_ids,
cover_candidates=cover_candidates,
cover_url=cover_url,
)
def render_from_memory(