From 7b583afaf44f7ff397844d2f2a217ab2efe086c5 Mon Sep 17 00:00:00 2001 From: xiaoxia Date: Fri, 14 Aug 2026 17:04:28 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=BB=9F=E4=B8=80=E5=B0=81?= =?UTF-8?q?=E9=9D=A2=E7=94=9F=E6=88=90=E7=AE=A1=E9=81=93=20=E2=80=94=20?= =?UTF-8?q?=E4=BB=8E=E6=B8=B2=E6=9F=93=E5=90=8E=E8=A7=86=E9=A2=91=E6=8A=BD?= =?UTF-8?q?=E5=B8=A7=E4=BD=9C=E4=B8=BA=E5=B0=81=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 核心改动: - 渲染完成后从已渲染视频抽帧作为封面,天然带标题(ASS字幕已烧录) - 删除3条旧封面路径:缩略图、候选帧、FFmpeg备用抽帧 - 封面URL持久化到GenerationTask.cover_url - 封面API直接从持久化URL读取 --- packages/shared/ai_service.py | 227 ++-------------------------------- 1 file changed, 10 insertions(+), 217 deletions(-) diff --git a/packages/shared/ai_service.py b/packages/shared/ai_service.py index 2ae55042e..1a6e5f90c 100755 --- a/packages/shared/ai_service.py +++ b/packages/shared/ai_service.py @@ -354,93 +354,6 @@ def _transfer_cover_frame_to_storage(frame_url: str, plan_id: str) -> str: return frame_url -def _extract_frames_with_ffmpeg( - video_url: str, - num_frames: int = 3, - timeout: int = 30, -) -> list[dict]: - """用 FFmpeg 从远程视频 URL 流式 seek 抽帧(HTTP range request,不下载整个视频)。 - - Args: - video_url: 视频 URL - num_frames: 抽帧数量 - timeout: 单帧超时(秒) - - Returns: - [{"local_path": "...", "frame_time": 5.0}, ...] - """ - import re as _re - import tempfile - from pathlib import Path as _Path - - from packages.shared.ffmpeg_utils import FFMPEG_BIN, run_ffmpeg - - video_url = _re.sub(r"(? 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("FFmpeg 远程抽帧失败 ratio=%.2f: %s", ratio, e) - _Path(output_path).unlink(missing_ok=True) - - return results - - def _call_ai_cover_service( plan_id: str, asset_ids: List[str], @@ -450,9 +363,9 @@ def _call_ai_cover_service( ) -> Dict[str, Any]: """调用 AI 封面生成服务. - 优先级: - 1. 检查 plan.config 中的 cover_candidates(渲染时预抽帧)——由调用方处理 - 2. FFmpeg 本地从 URL 流式 seek 抽帧(HTTP range request,不下载整个视频) + 统一封面管道下,封面已由渲染后视频抽帧生成并持久化到 GenerationTask.cover_url。 + 此函数仅处理 manual/upload 等需要前端交互的类型, + ai_frame/ai_regenerate 类型应由调用方直接从持久化的封面 URL 读取。 失败时抛出 RuntimeError。 @@ -484,136 +397,16 @@ def _call_ai_cover_service( "frame_time": frame_time, } - # ai_frame / ai_regenerate - 使用 FFmpeg 本地抽帧 - if primary_video_url: - import re as _re - - primary_video_url = _re.sub(r"(? Dict[str, Any]: - """执行 AI 推荐片段方案 - - Args: - plan_id: 剪辑计划 ID - template_id: 模板 ID - asset_ids: 素材 ID 列表 - editing_mode: 剪辑模式 (one_take / pip / voice_over / voice_pip) - target_duration: 目标时长(秒) - asset_analyses: 可选,{asset_id: 视频理解文本} 映射 - - Returns: - 推荐方案 dict,包含 clips / config / total_duration / confidence - """ - logger.info( - "AI 推荐片段方案: plan_id=%s template_id=%s assets=%d mode=%s duration=%.1f has_analysis=%s", + # ai_frame / ai_regenerate: 封面应由渲染后视频抽帧管道生成 + # 如果调用方传入了持久化的封面 URL,直接使用 + logger.warning( + "封面生成回退: plan_id=%s cover_type=%s — 统一管道应已生成封面,请检查 GenerationTask.cover_url", plan_id, - template_id, - len(asset_ids), - editing_mode, - target_duration, - bool(asset_analyses), + cover_type, ) - result = _call_ai_recommend_service( - plan_id=plan_id, - template_id=template_id, - asset_ids=asset_ids, - editing_mode=editing_mode, - target_duration=target_duration, - asset_analyses=asset_analyses, + raise RuntimeError( + f"封面数据不可用 (plan_id={plan_id})。请重新生成预览视频以触发封面自动提取。" ) - logger.info( - "AI 推荐完成: plan_id=%s clips=%d duration=%.1f confidence=%.2f", - plan_id, - len(result["clips"]), - result["total_duration"], - result["confidence"], - ) - return result def run_generate_cover(