48d7e01498
Deploy / Build Production Runtime Images (push) Has been skipped
Deploy / Deploy Production (push) Has been skipped
Deploy / Production Browser E2E (push) Has been skipped
Deploy / Staging E2E Tests (push) Failing after 112h44m30s
Deploy / Deploy Staging (push) Failing after 112h47m19s
CI/CD Pipeline / Frontend Lint (push) Failing after 112h47m45s
CI/CD Pipeline / Validate Code Quality And Tests (push) Failing after 112h47m52s
新增接口:
- POST /api/v1/edit-plans/{plan_id}/ai-recommend — AI分析素材推荐片段编排
- POST /api/v1/edit-plans/{plan_id}/generate-cover — AI智能选帧/生成封面
config 标准化:
- packages/domain/config_schemas.py: cover/title/subtitle/bgm 完整 schema 定义
- normalize_plan_config() / normalize_template_config() 自动填充默认值
- edit_plans create/update 和 edit_templates create/update 均已接入标准化
AI任务:
- apps/worker/worker_app/tasks/ai_tasks.py: stub实现,后续替换为真实AI服务
- Celery lazy import 已注册 run_ai_recommend / run_generate_cover
测试: 33个新用例覆盖 schema验证、AI任务、API端点、config标准化
227 lines
6.5 KiB
Python
227 lines
6.5 KiB
Python
"""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
|