01e057c939
CI/CD Pipeline / Check if frontend-only change (push) Waiting to run
CI/CD Pipeline / Validate - Code Quality (push) Waiting to run
CI/CD Pipeline / Validate - Type Check (mypy) (push) Waiting to run
CI/CD Pipeline / Validate - Migration (alembic) (push) Waiting to run
CI/CD Pipeline / Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / Integration Tests (push) Blocked by required conditions
CI/CD Pipeline / Frontend Lint (push) Waiting to run
CI/CD Pipeline / Frontend Unit Tests (push) Blocked by required conditions
CI/CD Pipeline / PR Build API Image (push) Waiting to run
CI/CD Pipeline / PR Build Web Image (push) Waiting to run
CI/CD Pipeline / PR Build Worker Image (push) Waiting to run
CI/CD Pipeline / Build Staging API Image (push) Waiting to run
CI/CD Pipeline / Build Staging Web Image (push) Waiting to run
CI/CD Pipeline / Build Staging Worker Image (push) Waiting to run
CI/CD Pipeline / Build Production API Image (push) Waiting to run
CI/CD Pipeline / Build Production Web Image (push) Waiting to run
CI/CD Pipeline / Build Production Worker Image (push) Waiting to run
CI/CD Pipeline / Deploy Production (push) Blocked by required conditions
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / Production Browser E2E (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
251 lines
8.6 KiB
Python
Executable File
251 lines
8.6 KiB
Python
Executable File
"""草稿生成路由.
|
|
|
|
端点:
|
|
- POST /generate 触发生成
|
|
- GET /generation-status 生成进度
|
|
- GET /generations 生成记录列表
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
from typing import Any
|
|
|
|
from app.auth import AuthenticatedUser, get_current_user
|
|
from app.core.celery_app import celery_app
|
|
from app.core.storage import OSSStorageService, get_storage_service
|
|
from app.dependencies import (
|
|
get_asset_library_repository,
|
|
get_asset_repository,
|
|
get_db_session,
|
|
)
|
|
from app.schemas.generation_task import GenerationTaskResponse
|
|
from app.services.edit_plan_service import EditPlanService
|
|
from app.services.edit_template_service import EditTemplateService
|
|
from fastapi import APIRouter, Depends, HTTPException, status
|
|
from sqlalchemy.orm import Session
|
|
|
|
from packages.adapters.sqlalchemy_impl.generation_task_repository import (
|
|
SQLAlchemyGenerationTaskRepository,
|
|
)
|
|
from packages.application.generation_tasks import (
|
|
CreateGenerationTaskCommand,
|
|
CreateGenerationTaskUseCase,
|
|
)
|
|
from packages.domain.edit_plan import EditPlanStatus
|
|
|
|
from ._fallback import (
|
|
_auto_fallback_assign_assets,
|
|
_auto_fallback_auto_material_mode,
|
|
_auto_fallback_copy_template_clips,
|
|
_auto_fallback_draft_to_editing,
|
|
)
|
|
from .dependencies import _check_queue_limits, get_draft_plan_id, get_editor_services
|
|
from .schemas import (
|
|
ClipStatusItem,
|
|
EditPlanGenerateResponse,
|
|
EditPlanGenerationsResponse,
|
|
EditPlanGenerationStatusResponse,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
router = APIRouter(tags=["Template Editor"])
|
|
|
|
|
|
@router.post("/generate", response_model=EditPlanGenerateResponse)
|
|
def generate_editor_draft(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
db: Session = Depends(get_db_session),
|
|
current_user: AuthenticatedUser = Depends(get_current_user),
|
|
asset_library_repo: Any = Depends(get_asset_library_repository),
|
|
asset_repo: Any = Depends(get_asset_repository),
|
|
) -> EditPlanGenerateResponse:
|
|
"""触发模板草稿渲染生成"""
|
|
_, plan_svc = services
|
|
plan_check = plan_svc.get_plan_or_raise(plan_id)
|
|
|
|
# 自动兜底流程
|
|
_auto_fallback_draft_to_editing(plan_svc, plan_id, plan_check)
|
|
_auto_fallback_copy_template_clips(plan_svc, plan_id, plan_check, db)
|
|
clips_without_asset = _auto_fallback_assign_assets(plan_svc, plan_id, plan_check)
|
|
_auto_fallback_auto_material_mode(
|
|
plan_svc, plan_id, plan_check, clips_without_asset, asset_library_repo, asset_repo
|
|
)
|
|
|
|
# 检查是否可生成
|
|
try:
|
|
can_gen, reason = plan_svc.can_generate(plan_id)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
if not can_gen:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST, detail=reason
|
|
)
|
|
|
|
try:
|
|
clip_count = plan_svc.mark_clips_ready(plan_id)
|
|
|
|
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
|
|
user_id = current_user.user.id
|
|
_check_queue_limits(gen_task_repo, user_id)
|
|
|
|
gen_task_use_case = CreateGenerationTaskUseCase(gen_task_repo)
|
|
plan = plan_svc.get_plan_or_raise(plan_id)
|
|
config_asset_ids = (plan.config or {}).get("asset_ids", [])
|
|
gen_task = gen_task_use_case.execute(
|
|
CreateGenerationTaskCommand(
|
|
project_id=plan.project_id or "",
|
|
template_id=plan.template_id,
|
|
created_by_user_id=current_user.user.id,
|
|
source_edit_plan_id=plan_id,
|
|
asset_ids=list(config_asset_ids) if config_asset_ids else [],
|
|
),
|
|
)
|
|
|
|
plan_svc.update_plan_config(plan_id, {"generation_task_id": gen_task.id})
|
|
plan_svc.transition_status(plan_id, EditPlanStatus.RENDERING)
|
|
celery_app.send_task("worker.render_edit_plan", args=[plan_id])
|
|
|
|
updated_plan = plan_svc.get_plan_or_raise(plan_id)
|
|
|
|
logger.info(
|
|
"模板编辑器触发生成: template_id=%s plan_id=%s gen_task_id=%s clips=%d by user=%s",
|
|
template_id,
|
|
plan_id,
|
|
gen_task.id,
|
|
clip_count,
|
|
current_user.user.id,
|
|
)
|
|
|
|
return EditPlanGenerateResponse(
|
|
plan_id=plan_id,
|
|
plan_status=updated_plan.status.value
|
|
if hasattr(updated_plan.status, "value")
|
|
else updated_plan.status,
|
|
generation_task_id=gen_task.id,
|
|
clip_count=clip_count,
|
|
)
|
|
except HTTPException:
|
|
raise
|
|
except Exception as _e:
|
|
logger.exception(
|
|
"模板编辑器触发生成失败: template_id=%s plan_id=%s",
|
|
template_id,
|
|
plan_id,
|
|
)
|
|
try:
|
|
plan_svc.transition_status(plan_id, EditPlanStatus.FAILED)
|
|
except Exception:
|
|
pass
|
|
raise HTTPException(
|
|
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
|
|
detail="生成失败,请稍后重试",
|
|
) from _e
|
|
|
|
|
|
@router.get("/generation-status", response_model=EditPlanGenerationStatusResponse)
|
|
def get_editor_generation_status(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
storage_service: OSSStorageService = Depends(get_storage_service),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
) -> EditPlanGenerationStatusResponse:
|
|
"""查询草稿生成进度"""
|
|
_, plan_svc = services
|
|
try:
|
|
gen_status = plan_svc.get_generation_status(plan_id)
|
|
except ValueError as exc:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)
|
|
) from exc
|
|
|
|
plan = gen_status["plan"]
|
|
clips = gen_status["clips"]
|
|
|
|
clip_items = [
|
|
ClipStatusItem(
|
|
clip_id=c.id,
|
|
clip_type=c.clip_type,
|
|
order=c.order,
|
|
status=c.status.value if hasattr(c.status, "value") else c.status,
|
|
asset_id=c.asset_id or "",
|
|
text_content=c.text_content or "",
|
|
duration=c.duration,
|
|
)
|
|
for c in clips
|
|
]
|
|
|
|
raw_video_url = (plan.config or {}).get("rendered_url", "")
|
|
video_url = ""
|
|
if raw_video_url:
|
|
try:
|
|
video_url = storage_service.get_download_url(
|
|
raw_video_url, expires_seconds=86400
|
|
)
|
|
except Exception as e:
|
|
logger.warning(
|
|
"生成视频签名URL失败: template_id=%s error=%s", template_id, e
|
|
)
|
|
video_url = raw_video_url
|
|
|
|
progress = gen_status.get("progress", 0.0)
|
|
error_message = gen_status.get("error_message", "")
|
|
gen_task_status = gen_status.get("generation_task_status")
|
|
plan_status_val = (
|
|
plan.status.value if hasattr(plan.status, "value") else plan.status
|
|
)
|
|
if plan_status_val == "completed" and progress < 100:
|
|
progress = 100.0
|
|
|
|
return EditPlanGenerationStatusResponse(
|
|
plan_id=plan_id,
|
|
plan_status=plan_status_val,
|
|
generation_task_id=gen_status["generation_task_id"],
|
|
generation_task_status=gen_task_status,
|
|
progress=progress,
|
|
video_url=video_url,
|
|
error_message=error_message,
|
|
clips=clip_items,
|
|
)
|
|
|
|
|
|
@router.get("/generations", response_model=EditPlanGenerationsResponse)
|
|
def list_editor_generations(
|
|
template_id: str,
|
|
plan_id: str = Depends(get_draft_plan_id),
|
|
services: tuple[EditTemplateService, EditPlanService] = Depends(get_editor_services),
|
|
db: Session = Depends(get_db_session),
|
|
_: AuthenticatedUser = Depends(get_current_user),
|
|
) -> EditPlanGenerationsResponse:
|
|
"""查询草稿关联的生成记录列表"""
|
|
_, plan_svc = services
|
|
plan_svc.get_plan_or_raise(plan_id)
|
|
|
|
gen_task_repo = SQLAlchemyGenerationTaskRepository(db)
|
|
tasks = gen_task_repo.list_by_source_edit_plan(plan_id)
|
|
items = [
|
|
GenerationTaskResponse(
|
|
id=t.id,
|
|
project_id=t.project_id,
|
|
asset_library_id=t.asset_library_id,
|
|
strategy_id=t.strategy_id,
|
|
voice_library_id=t.voice_library_id,
|
|
template_id=t.template_id,
|
|
asset_ids=t.asset_ids,
|
|
title_ids=t.title_ids,
|
|
voice_ids=t.voice_ids,
|
|
source_edit_plan_id=t.source_edit_plan_id or "",
|
|
status=t.status.value if hasattr(t.status, "value") else t.status,
|
|
progress=t.progress,
|
|
result_count=t.result_count,
|
|
error_message=t.error_message,
|
|
)
|
|
for t in tasks
|
|
]
|
|
return EditPlanGenerationsResponse(items=items, total=len(items))
|