perf: pre-extract cover frames during render + FFmpeg fallback
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m26s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Failing after 2m43s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m34s
CI/CD Pipeline / Validate - Code Quality (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 / 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 / PR Build Web Image (pull_request) Failing after 811h30m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 811h31m23s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 811h31m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 811h30m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 811h30m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 812h4m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 812h4m55s
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 46s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 1m25s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m26s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m44s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Failing after 2m43s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 2m14s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 2m15s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m34s
CI/CD Pipeline / Validate - Code Quality (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 / 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 / PR Build Web Image (pull_request) Failing after 811h30m37s
CI/CD Pipeline / Staging API Integration Tests (pull_request) Failing after 811h31m23s
CI/CD Pipeline / Staging E2E Tests (pull_request) Failing after 811h31m23s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Failing after 811h30m37s
CI/CD Pipeline / Build Staging Worker Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / Frontend Lint (pull_request) Failing after 811h30m38s
CI/CD Pipeline / Build Staging Web Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / Build Staging API Image (pull_request) Failing after 811h31m25s
CI/CD Pipeline / ACR Image Cleanup (pull_request) Failing after 812h4m53s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Failing after 812h4m55s
- Add extract_cover_candidates() and extract_and_upload_cover_frames() to thumbnail_generator.py: extract 3 frames at 25%/50%/75% of video - Add cover_candidates field to RenderAdapterResult - Call frame extraction after thumbnail gen in render_adapter._do_render() - Write cover_candidates to plan.config in edit_plan_generation worker - Check pre-stored frames in generation_cover.py before calling AI service - Replace MediaKit remote API with FFmpeg HTTP range-request in _call_ai_cover_service (no full video download needed) - Fix CoverTemplateResponse ValidationError when config is None - Add unit tests for all new code paths - Update existing tests to match new FFmpeg-based behavior
This commit is contained in:
@@ -155,3 +155,122 @@ def generate_and_upload_thumbnail(
|
||||
Path(thumbnail_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def extract_cover_candidates(
|
||||
video_path: str,
|
||||
num_frames: int = 3,
|
||||
*,
|
||||
width: int = 640,
|
||||
timeout: int = 30,
|
||||
) -> list[dict]:
|
||||
"""在视频时长 25%/50%/75% 处各抽一帧,返回候选帧信息列表。
|
||||
|
||||
Args:
|
||||
video_path: 视频文件路径
|
||||
num_frames: 抽帧数量(默认 3)
|
||||
width: 输出宽度
|
||||
timeout: 单帧超时(秒)
|
||||
|
||||
Returns:
|
||||
[{"local_path": "...", "frame_time": 5.0}, ...]
|
||||
"""
|
||||
from video_processing.ffmpeg_utils import FFMPEG_BIN, probe_duration, run_ffmpeg
|
||||
|
||||
try:
|
||||
duration = probe_duration(video_path)
|
||||
except Exception:
|
||||
duration = 0.0
|
||||
|
||||
if duration <= 0:
|
||||
duration = 5.0 # fallback
|
||||
|
||||
# 计算抽帧时间点:25%, 50%, 75%
|
||||
ratios = []
|
||||
for i in range(1, num_frames + 1):
|
||||
ratios.append(i / (num_frames + 1))
|
||||
|
||||
results = []
|
||||
for idx, ratio in enumerate(ratios):
|
||||
frame_time = max(0.5, duration * ratio)
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".jpg", delete=False)
|
||||
tmp.close()
|
||||
output_path = tmp.name
|
||||
|
||||
try:
|
||||
seek_str = _format_seek_time(frame_time)
|
||||
scale_filter = f"scale={width}:-1:force_original_aspect_ratio=decrease,format=yuvj420p"
|
||||
cmd = [
|
||||
FFMPEG_BIN, "-y",
|
||||
"-ss", seek_str,
|
||||
"-i", video_path,
|
||||
"-vframes", "1",
|
||||
"-vf", scale_filter,
|
||||
"-q:v", "2",
|
||||
output_path,
|
||||
]
|
||||
run_ffmpeg(cmd, capture_output=True, timeout=timeout)
|
||||
|
||||
if Path(output_path).exists() and Path(output_path).stat().st_size > 0:
|
||||
results.append({
|
||||
"local_path": output_path,
|
||||
"frame_time": round(frame_time, 2),
|
||||
})
|
||||
else:
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
except Exception as e:
|
||||
logger.warning("封面候选帧抽取失败 ratio=%.2f: %s", ratio, e)
|
||||
Path(output_path).unlink(missing_ok=True)
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def extract_and_upload_cover_frames(
|
||||
video_path: str,
|
||||
plan_id: str,
|
||||
num_frames: int = 3,
|
||||
) -> list[dict]:
|
||||
"""抽取封面候选帧并上传到 OSS。
|
||||
|
||||
Args:
|
||||
video_path: 本地视频路径
|
||||
plan_id: 剪辑计划 ID(用于 OSS 路径)
|
||||
num_frames: 抽帧数量
|
||||
|
||||
Returns:
|
||||
[{"image_url": "https://...", "frame_time": 5.0, "storage_key": "covers/xxx/frame_0.jpg"}, ...]
|
||||
"""
|
||||
candidates = extract_cover_candidates(video_path, num_frames=num_frames)
|
||||
if not candidates:
|
||||
logger.warning("封面候选帧抽取为空: plan_id=%s", plan_id)
|
||||
return []
|
||||
|
||||
results = []
|
||||
for idx, cand in enumerate(candidates):
|
||||
local_path = cand["local_path"]
|
||||
frame_time = cand["frame_time"]
|
||||
storage_key = f"covers/{plan_id}/frame_{idx}.jpg"
|
||||
|
||||
try:
|
||||
from video_processing.oss_helpers import upload_to_oss
|
||||
|
||||
url = upload_to_oss(local_path, storage_key)
|
||||
if url:
|
||||
results.append({
|
||||
"image_url": url,
|
||||
"frame_time": frame_time,
|
||||
"storage_key": storage_key,
|
||||
})
|
||||
logger.info(
|
||||
"封面候选帧上传成功: plan_id=%s idx=%d frame_time=%.2f",
|
||||
plan_id, idx, frame_time,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning("封面候选帧上传失败: plan_id=%s idx=%d error=%s", plan_id, idx, e)
|
||||
finally:
|
||||
try:
|
||||
Path(local_path).unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return results
|
||||
|
||||
Reference in New Issue
Block a user