feat(worker): ffmpeg渲染失败时捕获stderr写入error_detail,增强排障能力
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 32s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 22s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
Auto Merge CI PRs / Auto Merge on CI Green + Approved (pull_request) Successful in 17m53s
Auto Approve CI PRs / Auto Approve on CI Green (pull_request) Successful in 19m11s
CI Build & Deploy Pipeline / Build Staging API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Web Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI Build & Deploy Pipeline / Build Production API Image (pull_request) Has been skipped
CI Build & Deploy Pipeline / Deploy Production (pull_request) Has been skipped
CI Build & Deploy Pipeline / Production Browser E2E (pull_request) Has been skipped
AI Code Review / AI Code Review (pull_request) Failing after 32s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 22s
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
Auto Merge CI PRs / Auto Merge on CI Green + Approved (pull_request) Successful in 17m53s
Auto Approve CI PRs / Auto Approve on CI Green (pull_request) Successful in 19m11s
- RenderAdapterResult新增error_detail字段存完整stderr - render_adapter单独捕获CalledProcessError,把stderr写入error_message和error_detail - edit_plan_generation失败时把error_detail拼进gen_task.error_message - 新增test_ffmpeg_error_captures_stderr测试 - 35个render_adapter测试全绿
This commit is contained in:
@@ -16,6 +16,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
import tempfile
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
@@ -70,6 +71,7 @@ class RenderAdapterResult:
|
||||
rendered_clip_ids: list[str] = None # 成功渲染的 clip id 列表
|
||||
failed_clip_ids: list[str] = None # 失败的 clip id 列表
|
||||
error_message: str = ""
|
||||
error_detail: str = "" # 详细错误信息(如 ffmpeg stderr),用于排查
|
||||
|
||||
def __post_init__(self):
|
||||
if self.rendered_clip_ids is None:
|
||||
@@ -249,6 +251,20 @@ class RenderAdapter:
|
||||
failed_clip_ids=failed_clip_ids,
|
||||
)
|
||||
|
||||
except subprocess.CalledProcessError as exc:
|
||||
stderr_text = (exc.stderr or "").strip()
|
||||
logger.error(
|
||||
"[render-adapter] ffmpeg渲染失败: plan_id=%s job_id=%s exit_code=%d\nstderr:\n%s",
|
||||
plan_id,
|
||||
job_id,
|
||||
exc.returncode,
|
||||
stderr_text[:3000],
|
||||
)
|
||||
return RenderAdapterResult(
|
||||
success=False,
|
||||
error_message=f"FFmpeg渲染失败(exit={exc.returncode}): {stderr_text[:200]}",
|
||||
error_detail=stderr_text[:3000],
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.exception(
|
||||
"[render-adapter] render failed: plan_id=%s job_id=%s engine=unified error=%s",
|
||||
|
||||
@@ -265,8 +265,11 @@ def _render_with_unified(
|
||||
return {"status": "error", "message": f"渲染失败: {render_err}"}
|
||||
|
||||
if not result.success:
|
||||
full_error = result.error_message or "渲染失败"
|
||||
if result.error_detail:
|
||||
full_error = f"{full_error}\n--- stderr ---\n{result.error_detail}"
|
||||
logger.error("渲染失败(unified): %s — %s", plan_id, result.error_message)
|
||||
_mark_plan_failed(plan_repo, plan_id, gen_task_repo, generation_task_id, result.error_message or "渲染失败")
|
||||
_mark_plan_failed(plan_repo, plan_id, gen_task_repo, generation_task_id, full_error)
|
||||
return {"status": "error", "message": result.error_message or "渲染失败"}
|
||||
|
||||
output_path = result.output_path or Path("")
|
||||
|
||||
@@ -393,6 +393,40 @@ class TestRenderPlan:
|
||||
assert call_kwargs.kwargs["output_width"] == 1080
|
||||
assert call_kwargs.kwargs["output_height"] == 1920
|
||||
|
||||
@patch("video_processing.render_adapter.UnifiedRenderService")
|
||||
@patch("video_processing.render_adapter.download_asset")
|
||||
def test_ffmpeg_error_captures_stderr(self, mock_download, mock_render_cls, tmp_path):
|
||||
"""ffmpeg CalledProcessError 时 stderr 写入 error_detail 和 error_message。"""
|
||||
import subprocess
|
||||
|
||||
def _fake_download(storage_key, local_path):
|
||||
local_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
local_path.write_bytes(b"fake data")
|
||||
return True
|
||||
|
||||
mock_download.side_effect = _fake_download
|
||||
|
||||
mock_render = MagicMock()
|
||||
mock_render.render.side_effect = subprocess.CalledProcessError(
|
||||
returncode=1,
|
||||
cmd=["ffmpeg", "-i", "input.mp4", "output.mp4"],
|
||||
stderr="Invalid data found when processing input\nLast message repeated 3 times",
|
||||
)
|
||||
mock_render_cls.return_value = mock_render
|
||||
|
||||
plan = FakePlan(id="plan_001", status="editing")
|
||||
clips = [_make_clip("c1", order=0, duration=5.0)]
|
||||
asset_url_map = {"asset_c1.mp4": "https://test-bucket.oss.com/assets/asset_c1.mp4"}
|
||||
adapter, _, _ = _make_adapter(plan=plan, clips=clips, asset_url_map=asset_url_map)
|
||||
|
||||
result = adapter.render_plan("plan_001", job_id="job_001", work_dir=tmp_path / "work")
|
||||
|
||||
assert not result.success
|
||||
assert "FFmpeg渲染失败" in result.error_message
|
||||
assert "exit=1" in result.error_message
|
||||
assert "Invalid data found" in result.error_detail
|
||||
assert "Invalid data found" in result.error_message
|
||||
|
||||
@patch("video_processing.render_adapter.download_asset")
|
||||
def test_progress_callback(self, mock_download, tmp_path):
|
||||
"""进度回调被正确触发。"""
|
||||
|
||||
Reference in New Issue
Block a user