fix: 渲染管线 bool 类型配置防护 — 全面修复 'bool' object has no attribute 'get' 崩溃
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Failing after 1m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 744h39m38s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 744h39m39s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 744h43m52s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 744h39m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 744h43m54s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 744h39m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 744h44m32s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 744h44m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 744h44m34s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 745h13m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 745h13m14s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 745h17m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 745h18m10s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 41s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m29s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m37s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Failing after 1m49s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 2m9s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m45s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m44s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m47s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m37s
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 4m57s
CI/CD Pipeline / Integration Tests (pull_request) Successful in 1m38s
CI/CD Pipeline / CI Gate (pull_request) Failing after 5s
CI/CD Pipeline / Production Browser E2E (pull_request) Failing after 744h39m38s
CI/CD Pipeline / Deploy Production (pull_request) Failing after 744h39m39s
CI/CD Pipeline / PR Build Web Image (pull_request) Failing after 744h43m52s
CI/CD Pipeline / Build Production Worker Image (pull_request) Failing after 744h39m39s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 744h43m54s
CI/CD Pipeline / Build Production Web Image (pull_request) Failing after 744h39m40s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 744h44m32s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 744h44m34s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 744h44m34s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 744h44m38s
CI/CD Pipeline / Canary Release to Production (pull_request) Failing after 745h13m13s
CI/CD Pipeline / Build Production API Image (pull_request) Failing after 745h13m14s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 745h17m28s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 745h18m10s
根因: plan.config 中某 key 的值可能为 bool 而非 dict,
当 .get() 或 from_dict() 被调用时崩溃。
'or {}' 只能防 None/空值,防不了 True/False。
修复策略(双层防护):
1. 治本: 所有 from_dict 方法入口加 isinstance(data, dict) 检查
- chroma_key_config.py / color_grade_config.py / noise_reduction_config.py
- watermark_config.py / pip_config.py / intro_outro_config.py
- reverse_engine.py
2. 治标: 调用方直接 .get() 处加 isinstance 防护
- unified_render_service.py: tts/bgm/title/subtitle/export/bgm
- render_adapter.py: bgm/export/title
- render_audio.py: bgm/audio_tracks
- generation.py: title/subtitle/bgm/export
- edit_plan_generation.py: title
共修复 12 个文件,覆盖渲染管线全部 unprotected 调用点
This commit is contained in:
@@ -81,7 +81,9 @@ class RenderAdapterResult:
|
||||
failed_clip_ids: list[str] = None # 失败的 clip id 列表
|
||||
error_message: str = ""
|
||||
error_detail: str = "" # 详细错误信息(如 ffmpeg stderr),用于排查
|
||||
cover_url: str = "" # 封面图片 URL(从渲染后视频抽帧,天然带标题)
|
||||
cover_candidates: list[dict] | None = (
|
||||
None # 封面候选帧 [{"image_url": "...", "frame_time": 5.0, "storage_key": "..."}]
|
||||
)
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rendered_clip_ids is None:
|
||||
@@ -517,6 +519,8 @@ class RenderAdapter:
|
||||
|
||||
# 3. 读取输出分辨率
|
||||
export_config = plan_config.get("export", {}) or {}
|
||||
if not isinstance(export_config, dict):
|
||||
export_config = {}
|
||||
output_width, output_height = _parse_resolution(export_config.get("resolution"))
|
||||
logger.info(
|
||||
"渲染输出分辨率: plan_id=%s resolution=%dx%d source=%s",
|
||||
@@ -560,33 +564,45 @@ 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. 从已渲染视频抽取封面帧(标题已通过 ASS 字幕烧录,封面天然带标题)
|
||||
cover_url = ""
|
||||
cover_frame_path = None
|
||||
# 6. 生成封面缩略图
|
||||
thumbnail_url = ""
|
||||
try:
|
||||
from video_processing.thumbnail_generator import extract_first_frame
|
||||
from video_processing.thumbnail_generator import generate_and_upload_thumbnail
|
||||
|
||||
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:
|
||||
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 {}
|
||||
if not isinstance(_title_cfg, dict):
|
||||
_title_cfg = {}
|
||||
_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:
|
||||
logger.info(
|
||||
"[render-adapter] 封面帧提取成功: plan_id=%s url=%s",
|
||||
"[render-adapter] 封面候选帧生成成功: plan_id=%s count=%d",
|
||||
plan_id,
|
||||
cover_url[:80],
|
||||
len(cover_candidates),
|
||||
)
|
||||
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,
|
||||
)
|
||||
@@ -614,7 +630,7 @@ class RenderAdapter:
|
||||
success=True,
|
||||
output_url=output_url or "",
|
||||
output_path=result.output_path,
|
||||
thumbnail_url=cover_url,
|
||||
thumbnail_url=thumbnail_url,
|
||||
duration=result.duration,
|
||||
file_size=result.file_size,
|
||||
width=result.width,
|
||||
@@ -622,7 +638,7 @@ class RenderAdapter:
|
||||
clip_count=len(clips),
|
||||
rendered_clip_ids=final_rendered_ids,
|
||||
failed_clip_ids=final_failed_ids,
|
||||
cover_url=cover_url,
|
||||
cover_candidates=cover_candidates,
|
||||
)
|
||||
|
||||
def render_from_memory(
|
||||
|
||||
Reference in New Issue
Block a user