Files
xiaoxia-saas/apps/worker/worker_app/tasks/ai_tasks.py
T
灵应 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
feat(edit-plans): AI推荐片段方案 + 封面生成接口 + config JSON schema 标准化
新增接口:
- 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标准化
2026-07-04 20:06:30 +08:00

227 lines
6.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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