4162168161
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Build Staging Web Image (push) Successful in 52s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 4m13s
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 5m46s
CI/CD Pipeline / Unit Tests (push) Failing after 6m10s
CI/CD Pipeline / Frontend Lint (push) Successful in 6m16s
CI/CD Pipeline / Integration Tests (push) Successful in 2m45s
CI/CD Pipeline / Build Staging API Image (push) Successful in 7m15s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 9m4s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 46s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 42s
CI/CD Pipeline / Staging E2E Tests (push) Failing after 57s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m12s
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
206 lines
6.5 KiB
Python
206 lines
6.5 KiB
Python
"""剪辑计划 AI 推荐 & 封面生成 API 端点。
|
||
|
||
从 edit_plans.py 拆分,包含:
|
||
- POST /{plan_id}/ai-recommend AI 推荐片段方案
|
||
- POST /{plan_id}/generate-cover AI 生成封面
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Any
|
||
|
||
from app.api.routes._helpers import check_project_access
|
||
from app.api.routes.edit_plans import (
|
||
AIRecommendClipItem,
|
||
AIRecommendRequest,
|
||
AIRecommendResponse,
|
||
GenerateCoverRequest,
|
||
GenerateCoverResponse,
|
||
)
|
||
from app.auth import AuthenticatedUser, get_current_user
|
||
from app.dependencies import get_db_session, get_project_repository
|
||
from app.services import EditPlanService
|
||
from fastapi import APIRouter, Depends, HTTPException, status
|
||
from sqlalchemy.orm import Session
|
||
|
||
from packages.domain.config_schemas import normalize_plan_config
|
||
|
||
from ._helpers import deprecated_edit_plans_api
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
router = APIRouter(
|
||
dependencies=[Depends(deprecated_edit_plans_api)],
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/{plan_id}/ai-recommend",
|
||
response_model=AIRecommendResponse,
|
||
deprecated=True,
|
||
)
|
||
def ai_recommend_clips(
|
||
plan_id: str,
|
||
body: AIRecommendRequest,
|
||
db: Session = Depends(get_db_session),
|
||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
) -> AIRecommendResponse:
|
||
"""AI 推荐片段方案
|
||
|
||
调用 AI 服务分析素材,自动生成片段编排方案并写入剪辑计划。
|
||
|
||
流程:
|
||
1. 验证计划存在且状态为 draft/editing
|
||
2. 调用 AI 推荐服务(当前为 stub,后续接入真实 AI)
|
||
3. 清除计划现有片段,按推荐方案重新创建
|
||
4. 更新计划 config(cover/title/subtitle/bgm)和 total_duration
|
||
5. 返回推荐方案详情
|
||
"""
|
||
svc = EditPlanService(db)
|
||
|
||
try:
|
||
plan = svc.get_plan_or_raise(plan_id)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||
|
||
if plan.project_id:
|
||
check_project_access(plan.project_id, current_user.user.id, project_repository)
|
||
|
||
plan_status = plan.status.value if hasattr(plan.status, "value") else plan.status
|
||
if plan_status not in ("draft", "editing"):
|
||
raise HTTPException(
|
||
status_code=status.HTTP_400_BAD_REQUEST,
|
||
detail="当前计划状态不支持AI推荐,请先创建或编辑计划后再试",
|
||
)
|
||
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_ai_recommend
|
||
|
||
result = run_ai_recommend(
|
||
plan_id=plan_id,
|
||
template_id=plan.template_id,
|
||
asset_ids=body.asset_ids,
|
||
editing_mode=body.editing_mode,
|
||
target_duration=body.target_duration,
|
||
)
|
||
|
||
# 事务保护:清除 → 重建 → 更新 必须在同一逻辑事务中
|
||
try:
|
||
svc.delete_all_clips(plan_id)
|
||
|
||
for clip_data in result["clips"]:
|
||
svc.create_clip(
|
||
plan_id=plan_id,
|
||
clip_type=clip_data["clip_type"],
|
||
order=clip_data["order"],
|
||
text_content=clip_data.get("text_content", ""),
|
||
duration=clip_data["duration"],
|
||
transition_effect=clip_data.get("transition_effect", "cut"),
|
||
asset_id=clip_data.get("asset_id", ""),
|
||
start_time=clip_data.get("start_time", 0.0),
|
||
config=clip_data.get("config", {}),
|
||
)
|
||
|
||
normalized_config = normalize_plan_config(result.get("config", {}))
|
||
svc.update_plan(
|
||
plan_id,
|
||
config=normalized_config,
|
||
total_duration=result["total_duration"],
|
||
)
|
||
except Exception as _e:
|
||
logger.exception("AI 推荐写入失败,plan_id=%s 数据可能不一致", plan_id)
|
||
try:
|
||
db.rollback()
|
||
except Exception as rollback_err:
|
||
logger.error(
|
||
"AI 推荐回滚失败,数据库会话可能处于不一致状态: plan_id=%s error=%s",
|
||
plan_id,
|
||
rollback_err,
|
||
)
|
||
raise HTTPException(
|
||
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
||
detail="AI推荐结果保存失败,请稍后重试",
|
||
) from _e
|
||
|
||
logger.info(
|
||
"AI 推荐片段方案: plan_id=%s clips=%d duration=%.1f by user=%s",
|
||
plan_id,
|
||
len(result["clips"]),
|
||
result["total_duration"],
|
||
current_user.user.id,
|
||
)
|
||
|
||
return AIRecommendResponse(
|
||
plan_id=plan_id,
|
||
clips=[
|
||
AIRecommendClipItem(
|
||
clip_type=c["clip_type"],
|
||
order=c["order"],
|
||
text_content=c.get("text_content", ""),
|
||
duration=c["duration"],
|
||
transition_effect=c.get("transition_effect", "cut"),
|
||
asset_id=c.get("asset_id", ""),
|
||
start_time=c.get("start_time", 0.0),
|
||
config=c.get("config", {}),
|
||
)
|
||
for c in result["clips"]
|
||
],
|
||
config=normalized_config,
|
||
total_duration=result["total_duration"],
|
||
confidence=result["confidence"],
|
||
)
|
||
|
||
|
||
@router.post(
|
||
"/{plan_id}/generate-cover",
|
||
response_model=GenerateCoverResponse,
|
||
deprecated=True,
|
||
)
|
||
def generate_cover(
|
||
plan_id: str,
|
||
body: GenerateCoverRequest,
|
||
db: Session = Depends(get_db_session),
|
||
current_user: AuthenticatedUser = Depends(get_current_user),
|
||
project_repository: Any = Depends(get_project_repository),
|
||
) -> GenerateCoverResponse:
|
||
"""AI 生成封面
|
||
|
||
调用 AI 服务从视频中选帧或生成封面图,并更新计划 config.cover。
|
||
"""
|
||
svc = EditPlanService(db)
|
||
|
||
try:
|
||
plan = svc.get_plan_or_raise(plan_id)
|
||
except ValueError as exc:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
|
||
|
||
if plan.project_id:
|
||
check_project_access(plan.project_id, current_user.user.id, project_repository)
|
||
|
||
from apps.worker.worker_app.tasks.ai_tasks import run_generate_cover
|
||
|
||
cover_data = run_generate_cover(
|
||
plan_id=plan_id,
|
||
asset_ids=body.asset_ids,
|
||
cover_type=body.cover_type,
|
||
frame_time=body.frame_time,
|
||
)
|
||
|
||
current_config = dict(plan.config)
|
||
current_config["cover"] = cover_data
|
||
normalized = normalize_plan_config(current_config)
|
||
svc.update_plan_config(plan_id, {"cover": normalized["cover"]})
|
||
|
||
logger.info(
|
||
"AI 封面生成: plan_id=%s type=%s by user=%s",
|
||
plan_id,
|
||
body.cover_type,
|
||
current_user.user.id,
|
||
)
|
||
|
||
return GenerateCoverResponse(
|
||
plan_id=plan_id,
|
||
cover=cover_data,
|
||
)
|