"""AI 相关异步任务 — 智能推荐 & 封面生成. 提供两个 Celery 任务: - ai_recommend_clips: 分析素材并推荐片段编排方案 - generate_cover: 从视频中选帧或生成封面图 当前为 stub 实现(返回模拟数据),后续接入真实 AI 服务时 只需替换 _call_ai_recommend_service / _call_ai_cover_service 内部逻辑。 """ from __future__ import annotations import logging import random import time from typing import Any, Dict, List from packages.domain.config_schemas import DEFAULT_EDIT_PLAN_CONFIG logger = logging.getLogger(__name__) # ── AI 推荐片段方案 ────────────────────────────────────────────────────────── def _call_ai_recommend_service( plan_id: str, template_id: str, asset_ids: List[str], editing_mode: str, target_duration: float, ) -> Dict[str, Any]: """调用 AI 推荐服务(stub) TODO: 接入真实 AI 服务,分析素材内容并生成推荐方案。 当前返回基于模板规则的模拟推荐数据。 """ # 模拟 AI 分析耗时 time.sleep(0.5) # 根据素材数量生成推荐片段 clips: List[Dict[str, Any]] = [] order = 0 # 开场片段 clips.append( { "clip_type": "intro", "order": order, "text_content": "精彩看点", "duration": 3.0, "transition_effect": "fade", "asset_id": asset_ids[0] if asset_ids else "", "start_time": 0.0, "config": {}, } ) order += 1 # 为每个素材生成展示片段 per_clip_duration = max(2.0, (target_duration - 6.0) / max(len(asset_ids), 1)) for i, asset_id in enumerate(asset_ids): clips.append( { "clip_type": "showcase", "order": order, "text_content": f"展示片段 {i + 1}", "duration": round(per_clip_duration, 1), "transition_effect": "cut", "asset_id": asset_id, "start_time": 0.0, "config": {}, } ) order += 1 # 结尾 CTA clips.append( { "clip_type": "outro", "order": order, "text_content": "感谢观看", "duration": 3.0, "transition_effect": "fade", "asset_id": "", "start_time": 0.0, "config": {}, } ) # 生成推荐 config config = DEFAULT_EDIT_PLAN_CONFIG.copy() config["title"]["text"] = f"精选视频 — {len(asset_ids)} 个片段" config["title"]["ai_auto"] = True return { "clips": clips, "config": config, "total_duration": round(sum(c["duration"] for c in clips), 1), "confidence": round(random.uniform(0.75, 0.95), 2), } # ── AI 封面生成 ────────────────────────────────────────────────────────────── def _call_ai_cover_service( plan_id: str, asset_ids: List[str], cover_type: str, frame_time: float | None = None, ) -> Dict[str, Any]: """调用 AI 封面生成服务(stub) TODO: 接入真实 AI 服务,从视频中选帧或生成封面。 当前返回模拟封面数据。 """ # 模拟 AI 处理耗时 time.sleep(0.3) if cover_type == "upload": return { "type": "upload", "image_url": "", "message": "请上传封面图片", } if cover_type == "manual" and frame_time is not None: return { "type": "manual", "image_url": f"/api/v1/assets/placeholder/cover?time={frame_time}", "frame_time": frame_time, } # ai_frame / ai_regenerate return { "type": "ai_frame", "image_url": f"/api/v1/assets/placeholder/cover?plan={plan_id}", "frame_time": round(random.uniform(1.0, 10.0), 1), "confidence": round(random.uniform(0.80, 0.98), 2), } # ── 任务入口(供 Celery 调度或路由直接调用) ───────────────────────────────── def run_ai_recommend( plan_id: str, template_id: str, asset_ids: List[str], editing_mode: str = "one_take", target_duration: float = 30.0, ) -> 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: 目标时长(秒) Returns: 推荐方案 dict,包含 clips / config / total_duration / confidence """ logger.info( "AI 推荐片段方案: plan_id=%s template_id=%s assets=%d mode=%s duration=%.1f", plan_id, template_id, len(asset_ids), editing_mode, target_duration, ) 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, ) 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( plan_id: str, asset_ids: List[str], cover_type: str = "ai_frame", frame_time: float | None = None, ) -> Dict[str, Any]: """执行 AI 封面生成 Args: plan_id: 剪辑计划 ID asset_ids: 素材 ID 列表(用于确定视频来源) cover_type: 封面类型 (ai_frame / manual / upload / ai_regenerate) frame_time: 手动选帧时间点(仅 manual 模式使用) Returns: 封面数据 dict,包含 type / image_url / frame_time """ logger.info( "AI 封面生成: plan_id=%s type=%s assets=%d", plan_id, cover_type, len(asset_ids), ) result = _call_ai_cover_service( plan_id=plan_id, asset_ids=asset_ids, cover_type=cover_type, frame_time=frame_time, ) logger.info( "AI 封面生成完成: plan_id=%s type=%s url=%s", plan_id, result.get("type"), result.get("image_url", "")[:60], ) return result