709 lines
27 KiB
Python
709 lines
27 KiB
Python
from __future__ import annotations
|
|
|
|
from types import SimpleNamespace
|
|
|
|
from fastapi import APIRouter, Body, Depends, HTTPException, Path, Query
|
|
from sqlalchemy import inspect as sa_inspect
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.dependencies import get_current_user, get_db
|
|
from app.models.user import User
|
|
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateStepCodeEnum
|
|
from app.schemas.shot_replicate import (
|
|
ShotReplicateActionOut,
|
|
ShotReplicateDeleteOut,
|
|
ShotReplicateGenerateImagePromptRequest,
|
|
ShotReplicateGenerateImageRequest,
|
|
ShotReplicateGenerateVideoPromptRequest,
|
|
ShotReplicateGenerateVideoRequest,
|
|
ShotReplicateImagePromptUpdateRequest,
|
|
ShotReplicateMaterialUpdateRequest,
|
|
ShotReplicateSpecOut,
|
|
ShotReplicateTaskDetailOut,
|
|
ShotReplicateVideoPromptSchemaUpdateRequest,
|
|
ShotSegmentDetailOut,
|
|
ShotSegmentListOut,
|
|
ShotSegmentReplicationCreateRequest,
|
|
ShotSplitByAIOut,
|
|
ShotSplitByAIRequest,
|
|
ShotSplitCustomOut,
|
|
ShotSplitCustomRequest,
|
|
ShotTaskSetCreate,
|
|
ShotTaskSetDetailOut,
|
|
ShotTaskSetListOut,
|
|
)
|
|
from app.services.shot_replicate_flow_service import (
|
|
_get_project_for_user,
|
|
create_shot_replicate_project_from_segment,
|
|
delete_shot_replicate_project,
|
|
generate_image_from_prompt,
|
|
generate_video_from_prompt,
|
|
mark_shot_replicate_step_dispatch_failed,
|
|
project_to_detail_out,
|
|
submit_image_prompt_optimize,
|
|
submit_video_prompt_optimize,
|
|
update_shot_replicate_image_prompt,
|
|
update_shot_replicate_material_input,
|
|
update_shot_replicate_video_prompt_schema,
|
|
)
|
|
from app.services.shot_replicate_taskset_service import (
|
|
create_custom_segment,
|
|
create_segments_by_ai,
|
|
create_task_set,
|
|
get_segment_for_user,
|
|
list_segments,
|
|
list_task_sets,
|
|
segment_detail,
|
|
task_set_detail,
|
|
)
|
|
from app.services.module_generation_log_service import log_module_error, log_module_event_file
|
|
from app.services.module_async_recovery_service import (
|
|
TASK_SHOT_IMAGE_PROMPT,
|
|
TASK_SHOT_VIDEO_PROMPT,
|
|
register_module_step_task,
|
|
register_shot_segment_analysis_task,
|
|
register_shot_split_task,
|
|
register_shot_task_set_analysis_task,
|
|
)
|
|
from app.tasks.celery_app import celery_app
|
|
|
|
MODULE = ModuleCodeEnum.SHOT_REPLICATE.value
|
|
|
|
|
|
|
|
def _safe_user_id(user: object | None) -> str | None:
|
|
"""从 ORM 对象中安全取用户ID,避免 rollback/commit 后访问过期属性触发 MissingGreenlet。"""
|
|
if user is None:
|
|
return None
|
|
try:
|
|
value = getattr(user, "__dict__", {}).get("id")
|
|
if value is not None:
|
|
return str(value)
|
|
except Exception:
|
|
pass
|
|
try:
|
|
identity = sa_inspect(user).identity
|
|
if identity:
|
|
return str(identity[0])
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _safe_user_is_admin(user: object | None) -> bool:
|
|
"""安全判断管理员身份;如果对象属性已过期,保守按普通用户处理。"""
|
|
if user is None:
|
|
return False
|
|
try:
|
|
data = getattr(user, "__dict__", {})
|
|
if "is_admin" in data:
|
|
return bool(data.get("is_admin"))
|
|
except Exception:
|
|
pass
|
|
return False
|
|
|
|
|
|
def _user_context(user: object | None) -> SimpleNamespace:
|
|
return SimpleNamespace(id=_safe_user_id(user), is_admin=_safe_user_is_admin(user))
|
|
|
|
router = APIRouter(prefix="/shot-replications", tags=["shot-replications"])
|
|
|
|
|
|
|
|
|
|
def _log_api_error(
|
|
*,
|
|
event_type: str,
|
|
current_user: User | None = None,
|
|
project_id: str | None = None,
|
|
step_id: str | None = None,
|
|
message: str | None = None,
|
|
exc: BaseException | None = None,
|
|
detail: dict | None = None,
|
|
) -> None:
|
|
log_module_error(
|
|
module=MODULE,
|
|
event_type=event_type,
|
|
project_id=project_id,
|
|
step_id=step_id,
|
|
user_id=_safe_user_id(current_user),
|
|
message=message,
|
|
detail=detail,
|
|
exc=exc,
|
|
)
|
|
|
|
|
|
def _log_api_exception_from_locals(exc: BaseException, local_values: dict, message: str) -> None:
|
|
current_user = local_values.get("current_user")
|
|
project_id = local_values.get("project_id_value") or local_values.get("project_id") or local_values.get("task_set_id") or local_values.get("task_set_id_value")
|
|
step_id = local_values.get("step_id_value") or local_values.get("step_id") or local_values.get("segment_id") or local_values.get("segment_id_value")
|
|
req = local_values.get("req")
|
|
detail = {"api": local_values.get("__name__"), "request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None}
|
|
_log_api_error(
|
|
event_type="API_REQUEST_FAILED",
|
|
current_user=current_user if isinstance(current_user, User) else None,
|
|
project_id=str(project_id) if project_id else None,
|
|
step_id=str(step_id) if step_id else None,
|
|
message=message,
|
|
detail=detail,
|
|
exc=exc,
|
|
)
|
|
|
|
|
|
|
|
def _ensure_celery_enabled(*, current_user: User | None = None, project_id: str | None = None, step_id: str | None = None) -> None:
|
|
if celery_app is not None:
|
|
return
|
|
message = "Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker"
|
|
_log_api_error(
|
|
event_type="CELERY_DISABLED",
|
|
current_user=current_user,
|
|
project_id=project_id,
|
|
step_id=step_id,
|
|
message=message,
|
|
detail={"api": "shot_replicate"},
|
|
)
|
|
raise HTTPException(status_code=503, detail=message)
|
|
|
|
async def _reload_project_detail(db: AsyncSession, current_user: User, project_id: str) -> ShotReplicateTaskDetailOut:
|
|
project = await _get_project_for_user(
|
|
db,
|
|
project_id=project_id,
|
|
user=_user_context(current_user),
|
|
for_update=False,
|
|
populate_existing=True,
|
|
)
|
|
return await project_to_detail_out(db, project)
|
|
|
|
|
|
async def _mark_dispatch_failed_and_raise(
|
|
db: AsyncSession,
|
|
*,
|
|
current_user: User,
|
|
project_id: str,
|
|
step_id: str | None,
|
|
message: str,
|
|
) -> None:
|
|
if step_id:
|
|
try:
|
|
await mark_shot_replicate_step_dispatch_failed(
|
|
db,
|
|
current_user=_user_context(current_user),
|
|
project_id=project_id,
|
|
step_id=step_id,
|
|
error_message=message,
|
|
)
|
|
await db.commit()
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_error(
|
|
event_type="CELERY_DISPATCH_MARK_FAILED",
|
|
current_user=current_user,
|
|
project_id=project_id,
|
|
step_id=step_id,
|
|
message="Celery 投递失败后标记步骤失败也失败",
|
|
detail={"dispatch_error": message},
|
|
exc=exc,
|
|
)
|
|
log_module_error(
|
|
module=MODULE,
|
|
event_type="CELERY_DISPATCH_FAILED",
|
|
project_id=project_id,
|
|
step_id=step_id,
|
|
user_id=_safe_user_id(current_user),
|
|
message=message,
|
|
detail={"reason": "celery_dispatch_failed"},
|
|
error=message,
|
|
)
|
|
raise HTTPException(status_code=503, detail=message)
|
|
|
|
|
|
@router.get(
|
|
"/spec",
|
|
response_model=ShotReplicateSpecOut,
|
|
summary="查询拆镜复刻模块状态枚举和步骤 JSON 结构说明",
|
|
)
|
|
async def get_spec():
|
|
return ShotReplicateSpecOut()
|
|
|
|
|
|
@router.post(
|
|
"/task-sets",
|
|
response_model=ShotTaskSetDetailOut,
|
|
summary="创建拆镜总任务集并异步分析原视频",
|
|
)
|
|
async def create_shot_task_set(
|
|
req: ShotTaskSetCreate = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
task_set = await create_task_set(db, current_user=current_user, req=req)
|
|
task_set_id = task_set.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"创建拆镜总任务集失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"创建拆镜总任务集失败: {exc}")
|
|
|
|
try:
|
|
from app.tasks.shot_replicate_tasks import analyze_original_video
|
|
|
|
await register_shot_task_set_analysis_task(task_set_id)
|
|
analyze_original_video.apply_async(args=[task_set_id], queue="gen_chatapi_create", countdown=0)
|
|
except Exception as exc:
|
|
_log_api_error(
|
|
event_type="CELERY_DISPATCH_FAILED",
|
|
current_user=current_user,
|
|
project_id=task_set_id,
|
|
message=f"拆镜分析任务投递失败: {exc}",
|
|
detail={"task_set_id": task_set_id, "task": "analyze_original_video"},
|
|
exc=exc,
|
|
)
|
|
raise HTTPException(status_code=503, detail=f"拆镜分析任务投递失败: {exc}")
|
|
|
|
return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id)
|
|
|
|
|
|
@router.get(
|
|
"/task-sets",
|
|
response_model=ShotTaskSetListOut,
|
|
summary="查询拆镜总任务集列表",
|
|
)
|
|
async def list_shot_task_sets(
|
|
status: str | None = Query(None, description="总任务状态,见 ShotTaskSetStatusEnum"),
|
|
analysis_status: str | None = Query(None, description="分析状态,见 ShotAnalysisStatusEnum"),
|
|
split_status: str | None = Query(None, description="拆镜状态,见 ShotSplitStatusEnum"),
|
|
keyword: str | None = Query(None, description="标题/内容关键词"),
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await list_task_sets(
|
|
db,
|
|
current_user=current_user,
|
|
status=status,
|
|
analysis_status=analysis_status,
|
|
split_status=split_status,
|
|
keyword=keyword,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/task-sets/{task_set_id}",
|
|
response_model=ShotTaskSetDetailOut,
|
|
summary="获取拆镜总任务集详情",
|
|
)
|
|
async def get_shot_task_set(
|
|
task_set_id: str = Path(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id)
|
|
|
|
|
|
@router.post(
|
|
"/task-sets/{task_set_id}/split-by-ai",
|
|
response_model=ShotSplitByAIOut,
|
|
summary="按 AI 建议方案异步拆镜",
|
|
)
|
|
async def split_by_ai(
|
|
task_set_id: str = Path(...),
|
|
req: ShotSplitByAIRequest = Body(default_factory=ShotSplitByAIRequest),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
out = await create_segments_by_ai(db, current_user=current_user, task_set_id=task_set_id, req=req)
|
|
segment_ids = [item.id for item in out.segments]
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"按 AI 建议拆镜失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"按 AI 建议拆镜失败: {exc}")
|
|
|
|
from app.tasks.shot_replicate_tasks import split_one_segment
|
|
|
|
for segment_id in segment_ids:
|
|
await register_shot_split_task(segment_id, task_set_id=task_set_id)
|
|
split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
|
|
return out
|
|
|
|
|
|
@router.post(
|
|
"/task-sets/{task_set_id}/split-custom",
|
|
response_model=ShotSplitCustomOut,
|
|
summary="按用户自定义开始/结束秒异步拆单条片段",
|
|
)
|
|
async def split_custom(
|
|
task_set_id: str = Path(...),
|
|
req: ShotSplitCustomRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
out = await create_custom_segment(db, current_user=current_user, task_set_id=task_set_id, req=req)
|
|
segment_id = out.segment.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"自定义拆镜失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"自定义拆镜失败: {exc}")
|
|
|
|
from app.tasks.shot_replicate_tasks import split_one_segment
|
|
|
|
await register_shot_split_task(segment_id, task_set_id=task_set_id)
|
|
split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
|
|
return out
|
|
|
|
|
|
@router.get(
|
|
"/task-sets/{task_set_id}/segments",
|
|
response_model=ShotSegmentListOut,
|
|
summary="查询拆镜片段列表",
|
|
)
|
|
async def list_task_set_segments(
|
|
task_set_id: str = Path(...),
|
|
source_mode: str | None = Query(None, description="ai_suggestion/custom"),
|
|
split_status: str | None = Query(None, description="拆镜状态"),
|
|
analysis_status: str | None = Query(None, description="片段分析状态"),
|
|
replicate_status: str | None = Query(None, description="复刻状态"),
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await list_segments(
|
|
db,
|
|
current_user=current_user,
|
|
task_set_id=task_set_id,
|
|
source_mode=source_mode,
|
|
split_status=split_status,
|
|
analysis_status=analysis_status,
|
|
replicate_status=replicate_status,
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/segments/{segment_id}",
|
|
response_model=ShotSegmentDetailOut,
|
|
summary="获取拆镜片段详情",
|
|
)
|
|
async def get_segment(
|
|
segment_id: str = Path(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await segment_detail(db, current_user=current_user, segment_id=segment_id)
|
|
|
|
|
|
@router.post(
|
|
"/segments/{segment_id}/replication-projects",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="将拆镜片段创建为拆镜复刻项目",
|
|
)
|
|
async def create_replication_project_from_segment(
|
|
segment_id: str = Path(...),
|
|
req: ShotSegmentReplicationCreateRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user, for_update=True)
|
|
project = await create_shot_replicate_project_from_segment(db, current_user=current_user, segment=segment, req=req)
|
|
project_id = project.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"创建拆镜复刻项目失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"创建拆镜复刻项目失败: {exc}")
|
|
|
|
return ShotReplicateActionOut(
|
|
message="已从拆镜片段创建复刻项目,素材视频已锁定",
|
|
project_id=project_id,
|
|
step_id=None,
|
|
detail=await _reload_project_detail(db, current_user, project_id),
|
|
)
|
|
|
|
|
|
@router.get(
|
|
"/projects/{project_id}",
|
|
response_model=ShotReplicateTaskDetailOut,
|
|
summary="获取拆镜复刻项目详情",
|
|
)
|
|
async def get_project(
|
|
project_id: str = Path(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
return await _reload_project_detail(db, current_user, project_id)
|
|
|
|
|
|
@router.put(
|
|
"/projects/{project_id}/material",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="修改拆镜复刻素材信息,素材视频不允许修改",
|
|
)
|
|
async def update_material(
|
|
project_id: str = Path(...),
|
|
req: ShotReplicateMaterialUpdateRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
project_id_value, step_id_value = await update_shot_replicate_material_input(db, current_user=current_user, project_id=project_id, req=req)
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"修改素材输入失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"修改素材输入失败: {exc}")
|
|
return ShotReplicateActionOut(message="素材输入已修改,素材视频保持锁定", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.put(
|
|
"/projects/{project_id}/steps/{step_id}/image-prompt",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="直接修改图片 AI 优化提词",
|
|
)
|
|
async def update_image_prompt(
|
|
project_id: str = Path(...),
|
|
step_id: str = Path(...),
|
|
req: ShotReplicateImagePromptUpdateRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
project, step = await update_shot_replicate_image_prompt(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req)
|
|
project_id_value, step_id_value = project.id, step.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"修改图片 AI 提词失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"修改图片 AI 提词失败: {exc}")
|
|
return ShotReplicateActionOut(message="图片 AI 提词已修改,后续步骤已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.put(
|
|
"/projects/{project_id}/steps/{step_id}/video-prompt-schema",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="修改视频 AI 提词 JSON schema",
|
|
)
|
|
async def update_video_prompt_schema(
|
|
project_id: str = Path(...),
|
|
step_id: str = Path(...),
|
|
req: ShotReplicateVideoPromptSchemaUpdateRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
project, step = await update_shot_replicate_video_prompt_schema(db, current_user=current_user, project_id=project_id, step_id=step_id, req=req)
|
|
project_id_value, step_id_value = project.id, step.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"修改视频 AI 提词失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"修改视频 AI 提词失败: {exc}")
|
|
return ShotReplicateActionOut(message="视频 AI 提词 schema 已修改,第5步视频生成已软删除", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.post(
|
|
"/projects/{project_id}/generate-image-prompt",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="生成图片 AI 提词",
|
|
)
|
|
async def generate_image_prompt(
|
|
project_id: str = Path(...),
|
|
req: ShotReplicateGenerateImagePromptRequest = Body(default_factory=ShotReplicateGenerateImagePromptRequest),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
|
project_id_value, step_id_value = project.id, step.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"提交图片 AI 提词失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"提交图片 AI 提词失败: {exc}")
|
|
|
|
try:
|
|
from app.tasks.shot_replicate_flow_tasks import start_image_prompt_optimize
|
|
|
|
await register_module_step_task(
|
|
module=MODULE,
|
|
project_id=project_id_value,
|
|
step_id=step_id_value,
|
|
step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
|
task_name=TASK_SHOT_IMAGE_PROMPT,
|
|
)
|
|
start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
|
except Exception as exc:
|
|
await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片 AI 提词任务投递失败: {exc}")
|
|
|
|
return ShotReplicateActionOut(message="图片 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.post(
|
|
"/projects/{project_id}/generate-image",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="根据图片 AI 提词生成图片",
|
|
)
|
|
async def generate_image(
|
|
project_id: str = Path(...),
|
|
req: ShotReplicateGenerateImageRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
|
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"提交图片生成失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"提交图片生成失败: {exc}")
|
|
|
|
try:
|
|
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
|
|
|
chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
|
|
except Exception as exc:
|
|
await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"图片生成任务投递失败: {exc}")
|
|
|
|
return ShotReplicateActionOut(message="图片生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.post(
|
|
"/projects/{project_id}/generate-video-prompt",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="生成视频 AI 提词 JSON schema",
|
|
)
|
|
async def generate_video_prompt(
|
|
project_id: str = Path(...),
|
|
req: ShotReplicateGenerateVideoPromptRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
|
project_id_value, step_id_value = project.id, step.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"提交视频 AI 提词失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"提交视频 AI 提词失败: {exc}")
|
|
|
|
try:
|
|
from app.tasks.shot_replicate_flow_tasks import start_video_prompt_optimize
|
|
|
|
await register_module_step_task(
|
|
module=MODULE,
|
|
project_id=project_id_value,
|
|
step_id=step_id_value,
|
|
step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
|
task_name=TASK_SHOT_VIDEO_PROMPT,
|
|
)
|
|
start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
|
except Exception as exc:
|
|
await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频 AI 提词任务投递失败: {exc}")
|
|
|
|
return ShotReplicateActionOut(message="视频 AI 提词任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.post(
|
|
"/projects/{project_id}/generate-video",
|
|
response_model=ShotReplicateActionOut,
|
|
summary="根据视频 AI 提词生成视频",
|
|
)
|
|
async def generate_video(
|
|
project_id: str = Path(...),
|
|
req: ShotReplicateGenerateVideoRequest = Body(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
_ensure_celery_enabled(current_user=current_user, project_id=locals().get("project_id") or locals().get("task_set_id"))
|
|
try:
|
|
project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
|
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
|
await db.commit()
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"提交视频生成失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"提交视频生成失败: {exc}")
|
|
|
|
try:
|
|
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
|
|
|
chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
|
|
except Exception as exc:
|
|
await _mark_dispatch_failed_and_raise(db, current_user=current_user, project_id=project_id_value, step_id=step_id_value, message=f"视频生成任务投递失败: {exc}")
|
|
|
|
return ShotReplicateActionOut(message="视频生成任务已提交", project_id=project_id_value, step_id=step_id_value, detail=await _reload_project_detail(db, current_user, project_id_value))
|
|
|
|
|
|
@router.delete(
|
|
"/projects/{project_id}",
|
|
response_model=ShotReplicateDeleteOut,
|
|
summary="软删除拆镜复刻项目",
|
|
)
|
|
async def delete_project(
|
|
project_id: str = Path(...),
|
|
current_user: User = Depends(get_current_user),
|
|
db: AsyncSession = Depends(get_db),
|
|
):
|
|
try:
|
|
out = await delete_shot_replicate_project(db, current_user=current_user, project_id=project_id)
|
|
await db.commit()
|
|
return out
|
|
except HTTPException:
|
|
await db.rollback()
|
|
raise
|
|
except Exception as exc:
|
|
await db.rollback()
|
|
_log_api_exception_from_locals(exc, locals(), f"删除拆镜复刻项目失败: {exc}")
|
|
raise HTTPException(status_code=500, detail=f"删除拆镜复刻项目失败: {exc}")
|