爆款开头复刻/拆镜复刻追加日志|拆镜复刻追加视频AI分析API

This commit is contained in:
2026-07-07 19:52:12 +08:00
parent 35ffa317e7
commit 2d419b171e
16 changed files with 1153 additions and 91 deletions
@@ -10,7 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from app.dependencies import get_current_user, get_db from app.dependencies import get_current_user, get_db
from app.models.user import User from app.models.user import User
from app.enums.common import ModuleProjectStatusEnum from app.enums.common import ModuleProjectStatusEnum
from app.enums.hot_opening_replicate import HotOpeningStepCodeEnum, ModuleCodeEnum from app.enums.hot_opening_replicate import HotOpeningLogEventEnum, HotOpeningStepCodeEnum, ModuleCodeEnum
from app.schemas.hot_opening_replicate import ( from app.schemas.hot_opening_replicate import (
HotOpeningActionOut, HotOpeningActionOut,
HotOpeningDeleteOut, HotOpeningDeleteOut,
@@ -125,7 +125,7 @@ def _log_api_exception_from_locals(exc: BaseException, local_values: dict, messa
req = local_values.get("req") req = local_values.get("req")
detail = {"request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None} detail = {"request": req.model_dump() if hasattr(req, "model_dump") else str(req) if req is not None else None}
_log_api_error( _log_api_error(
event_type="API_REQUEST_FAILED", event_type=HotOpeningLogEventEnum.API_REQUEST_FAILED.value,
current_user=current_user if isinstance(current_user, User) else None, current_user=current_user if isinstance(current_user, User) else None,
project_id=str(project_id) if project_id else None, project_id=str(project_id) if project_id else None,
step_id=str(step_id) if step_id else None, step_id=str(step_id) if step_id else None,
@@ -172,7 +172,7 @@ async def _mark_dispatch_failed_and_raise(
except Exception as exc: except Exception as exc:
await db.rollback() await db.rollback()
_log_api_error( _log_api_error(
event_type="CELERY_DISPATCH_MARK_FAILED", event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_MARK_FAILED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -182,7 +182,7 @@ async def _mark_dispatch_failed_and_raise(
) )
log_module_error( log_module_error(
module=MODULE, module=MODULE,
event_type="CELERY_DISPATCH_FAILED", event_type=HotOpeningLogEventEnum.CELERY_DISPATCH_FAILED.value,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
user_id=_safe_user_id(current_user), user_id=_safe_user_id(current_user),
@@ -439,7 +439,7 @@ async def generate_image_prompt(
_ = req _ = req
if celery_app is None: if celery_app is None:
_log_api_error( _log_api_error(
event_type="CELERY_DISABLED", event_type=HotOpeningLogEventEnum.CELERY_DISABLED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -509,7 +509,7 @@ async def generate_image(
): ):
if celery_app is None: if celery_app is None:
_log_api_error( _log_api_error(
event_type="CELERY_DISABLED", event_type=HotOpeningLogEventEnum.CELERY_DISABLED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -575,7 +575,7 @@ async def generate_video_prompt(
): ):
if celery_app is None: if celery_app is None:
_log_api_error( _log_api_error(
event_type="CELERY_DISABLED", event_type=HotOpeningLogEventEnum.CELERY_DISABLED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -646,7 +646,7 @@ async def generate_video(
): ):
if celery_app is None: if celery_app is None:
_log_api_error( _log_api_error(
event_type="CELERY_DISABLED", event_type=HotOpeningLogEventEnum.CELERY_DISABLED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
+159 -5
View File
@@ -12,6 +12,7 @@ from app.models.user import User
from app.enums.shot_replicate import ( from app.enums.shot_replicate import (
ModuleCodeEnum, ModuleCodeEnum,
ShotAnalysisStatusEnum, ShotAnalysisStatusEnum,
ShotReplicateLogEventEnum,
ShotReplicateStepCodeEnum, ShotReplicateStepCodeEnum,
ShotSegmentAnalysisStatusEnum, ShotSegmentAnalysisStatusEnum,
ShotSegmentReplicateStatusEnum, ShotSegmentReplicateStatusEnum,
@@ -27,6 +28,8 @@ from app.schemas.shot_replicate import (
ShotReplicateGenerateVideoPromptRequest, ShotReplicateGenerateVideoPromptRequest,
ShotReplicateGenerateVideoRequest, ShotReplicateGenerateVideoRequest,
ShotReplicateImagePromptUpdateRequest, ShotReplicateImagePromptUpdateRequest,
ShotReanalyzeOut,
ShotReanalyzeRequest,
ShotReplicateMaterialUpdateRequest, ShotReplicateMaterialUpdateRequest,
ShotReplicateSpecOut, ShotReplicateSpecOut,
ShotReplicateTaskDetailOut, ShotReplicateTaskDetailOut,
@@ -65,6 +68,8 @@ from app.services.shot_replicate_taskset_service import (
get_segment_for_user, get_segment_for_user,
list_segments, list_segments,
list_task_sets, list_task_sets,
prepare_reanalyze_segment,
prepare_reanalyze_task_set,
segment_detail, segment_detail,
task_set_detail, task_set_detail,
) )
@@ -152,7 +157,7 @@ def _log_api_exception_from_locals(exc: BaseException, local_values: dict, messa
req = local_values.get("req") 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} 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( _log_api_error(
event_type="API_REQUEST_FAILED", event_type=ShotReplicateLogEventEnum.API_REQUEST_FAILED.value,
current_user=current_user if isinstance(current_user, User) else None, current_user=current_user if isinstance(current_user, User) else None,
project_id=str(project_id) if project_id else None, project_id=str(project_id) if project_id else None,
step_id=str(step_id) if step_id else None, step_id=str(step_id) if step_id else None,
@@ -168,7 +173,7 @@ def _ensure_celery_enabled(*, current_user: User | None = None, project_id: str
return return
message = "Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker" message = "Celery未启用:请配置 REDIS_URL 或 CELERY_BROKER_URL 后启动 worker"
_log_api_error( _log_api_error(
event_type="CELERY_DISABLED", event_type=ShotReplicateLogEventEnum.CELERY_DISABLED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -209,7 +214,7 @@ async def _mark_dispatch_failed_and_raise(
except Exception as exc: except Exception as exc:
await db.rollback() await db.rollback()
_log_api_error( _log_api_error(
event_type="CELERY_DISPATCH_MARK_FAILED", event_type=ShotReplicateLogEventEnum.CELERY_DISPATCH_MARK_FAILED.value,
current_user=current_user, current_user=current_user,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
@@ -219,7 +224,7 @@ async def _mark_dispatch_failed_and_raise(
) )
log_module_error( log_module_error(
module=MODULE, module=MODULE,
event_type="CELERY_DISPATCH_FAILED", event_type=ShotReplicateLogEventEnum.CELERY_DISPATCH_FAILED.value,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
user_id=_safe_user_id(current_user), user_id=_safe_user_id(current_user),
@@ -275,7 +280,7 @@ async def create_shot_task_set(
analyze_original_video.apply_async(args=[task_set_id], queue="gen_chatapi_create", countdown=0) analyze_original_video.apply_async(args=[task_set_id], queue="gen_chatapi_create", countdown=0)
except Exception as exc: except Exception as exc:
_log_api_error( _log_api_error(
event_type="CELERY_DISPATCH_FAILED", event_type=ShotReplicateLogEventEnum.CELERY_DISPATCH_FAILED.value,
current_user=current_user, current_user=current_user,
project_id=task_set_id, project_id=task_set_id,
message=f"拆镜分析任务投递失败: {exc}", message=f"拆镜分析任务投递失败: {exc}",
@@ -345,6 +350,79 @@ async def get_shot_task_set(
return await task_set_detail(db, current_user=_user_context(current_user), task_set_id=task_set_id) 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}/reanalyze",
response_model=ShotReanalyzeOut,
summary="重新投递原视频 AI 分析任务",
description="用于处理原视频分析失败或待处理的异常数据;重置分析状态后重新投递 analyze_original_video。",
)
async def reanalyze_task_set(
task_set_id: str = Path(..., description="拆镜总任务集ID,即 shot_replicate_task_sets.id"),
req: ShotReanalyzeRequest = Body(default_factory=ShotReanalyzeRequest, description="再次分析参数"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
_ensure_celery_enabled(current_user=current_user, project_id=task_set_id)
try:
out = await prepare_reanalyze_task_set(
db,
current_user=current_user,
task_set_id=task_set_id,
force=req.force,
reason=req.reason,
)
await db.commit()
except HTTPException as exc:
await db.rollback()
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.TASK_SET_REANALYZE_REJECTED.value,
project_id=task_set_id,
user_id=_safe_user_id(current_user),
message="原视频再次分析请求被拒绝",
detail={"task_set_id": task_set_id, "request": req.model_dump(), "http_status": exc.status_code, "detail": exc.detail},
error=str(exc.detail),
)
raise
except Exception as exc:
await db.rollback()
_log_api_error(
event_type=ShotReplicateLogEventEnum.TASK_SET_REANALYZE_FAILED.value,
current_user=current_user,
project_id=task_set_id,
message=f"原视频再次分析状态重置失败: {exc}",
detail={"task_set_id": task_set_id, "request": req.model_dump()},
exc=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)
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.TASK_SET_REANALYZE_SUBMITTED.value,
project_id=task_set_id,
user_id=_safe_user_id(current_user),
message="原视频再次分析任务已投递",
detail={"task_set_id": task_set_id, "task": "analyze_original_video", "request": req.model_dump()},
)
except Exception as exc:
_log_api_error(
event_type=ShotReplicateLogEventEnum.CELERY_DISPATCH_FAILED.value,
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}")
out.message = "原视频再次分析任务已提交"
return out
@router.post( @router.post(
"/task-sets/{task_set_id}/split-by-ai", "/task-sets/{task_set_id}/split-by-ai",
response_model=ShotSplitByAIOut, response_model=ShotSplitByAIOut,
@@ -457,6 +535,82 @@ async def get_segment(
return await segment_detail(db, current_user=current_user, segment_id=segment_id) return await segment_detail(db, current_user=current_user, segment_id=segment_id)
@router.post(
"/segments/{segment_id}/reanalyze",
response_model=ShotReanalyzeOut,
summary="重新投递切片视频 AI 分析任务",
description="用于处理自定义切片视频分析失败或待处理的异常数据;重置分析状态后重新投递 analyze_custom_segment_video。",
)
async def reanalyze_segment(
segment_id: str = Path(..., description="拆镜片段ID,即 shot_replicate_segments.id"),
req: ShotReanalyzeRequest = Body(default_factory=ShotReanalyzeRequest, description="再次分析参数"),
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
_ensure_celery_enabled(current_user=current_user, step_id=segment_id)
try:
out = await prepare_reanalyze_segment(
db,
current_user=current_user,
segment_id=segment_id,
force=req.force,
reason=req.reason,
)
task_set_id = out.task_set_id
await db.commit()
except HTTPException as exc:
await db.rollback()
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.SEGMENT_REANALYZE_REJECTED.value,
step_id=segment_id,
user_id=_safe_user_id(current_user),
message="切片视频再次分析请求被拒绝",
detail={"segment_id": segment_id, "request": req.model_dump(), "http_status": exc.status_code, "detail": exc.detail},
error=str(exc.detail),
)
raise
except Exception as exc:
await db.rollback()
_log_api_error(
event_type=ShotReplicateLogEventEnum.SEGMENT_REANALYZE_FAILED.value,
current_user=current_user,
step_id=segment_id,
message=f"切片视频再次分析状态重置失败: {exc}",
detail={"segment_id": segment_id, "request": req.model_dump()},
exc=exc,
)
raise HTTPException(status_code=500, detail=f"切片视频再次分析状态重置失败: {exc}")
try:
from app.tasks.shot_replicate_tasks import analyze_custom_segment_video
await register_shot_segment_analysis_task(segment_id, task_set_id=task_set_id)
analyze_custom_segment_video.apply_async(args=[segment_id], queue="gen_chatapi_create", countdown=0)
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.SEGMENT_REANALYZE_SUBMITTED.value,
project_id=task_set_id,
step_id=segment_id,
user_id=_safe_user_id(current_user),
message="切片视频再次分析任务已投递",
detail={"segment_id": segment_id, "task_set_id": task_set_id, "task": "analyze_custom_segment_video", "request": req.model_dump()},
)
except Exception as exc:
_log_api_error(
event_type=ShotReplicateLogEventEnum.CELERY_DISPATCH_FAILED.value,
current_user=current_user,
project_id=task_set_id,
step_id=segment_id,
message=f"切片视频再次分析任务投递失败: {exc}",
detail={"segment_id": segment_id, "task_set_id": task_set_id, "task": "analyze_custom_segment_video"},
exc=exc,
)
raise HTTPException(status_code=503, detail=f"切片视频再次分析任务投递失败: {exc}")
out.message = "切片视频再次分析任务已提交"
return out
@router.delete( @router.delete(
"/segments/{segment_id}", "/segments/{segment_id}",
response_model=ShotSegmentDeleteOut, response_model=ShotSegmentDeleteOut,
+1 -1
View File
@@ -228,7 +228,7 @@ class Settings(BaseSettings):
# 拆镜复刻配置。 # 拆镜复刻配置。
# 原始上传视频和拆镜片段都属于 uploads 素材域;只有 generate 生成结果走 token 验签。 # 原始上传视频和拆镜片段都属于 uploads 素材域;只有 generate 生成结果走 token 验签。
SHOT_ANALYSIS_TIMEOUT_SECONDS: int = 180 SHOT_ANALYSIS_TIMEOUT_SECONDS: int = 600
SHOT_ANALYSIS_TEMPERATURE: float = 0.1 SHOT_ANALYSIS_TEMPERATURE: float = 0.1
SHOT_ANALYSIS_MAX_TOKENS: int = 5000 SHOT_ANALYSIS_MAX_TOKENS: int = 5000
SHOT_ANALYSIS_VIDEO_FPS: float = 1.0 SHOT_ANALYSIS_VIDEO_FPS: float = 1.0
+24
View File
@@ -3,6 +3,30 @@ from __future__ import annotations
from enum import StrEnum from enum import StrEnum
class LogEventStatusEnum(StrEnum):
"""通用日志事件状态。"""
PENDING = "pending"
STARTED = "started"
SUBMITTED = "submitted"
SUCCESS = "success"
FAILED = "failed"
REJECTED = "rejected"
SKIPPED = "skipped"
WARNING = "warning"
class LogSourceEnum(StrEnum):
"""通用日志来源。"""
API = "api"
SERVICE = "service"
CELERY = "celery"
RECOVERY = "recovery"
REMOTE_API = "remote_api"
class ModuleProjectStatusEnum(StrEnum): class ModuleProjectStatusEnum(StrEnum):
"""通用模块项目状态。""" """通用模块项目状态。"""
@@ -30,3 +30,32 @@ class HotOpeningStepIOSchemaVersionEnum(StrEnum):
"""爆款开头复刻子任务 input_json/output_json 结构版本。""" """爆款开头复刻子任务 input_json/output_json 结构版本。"""
V1 = "hot_opening_step_io_v1" V1 = "hot_opening_step_io_v1"
class HotOpeningLogEventEnum(StrEnum):
"""爆款开头复刻模块业务日志事件。"""
API_REQUEST_RECEIVED = "HOT_OPENING_API_REQUEST_RECEIVED"
API_REQUEST_SUBMITTED = "HOT_OPENING_API_REQUEST_SUBMITTED"
API_REQUEST_REJECTED = "HOT_OPENING_API_REQUEST_REJECTED"
API_REQUEST_FAILED = "HOT_OPENING_API_REQUEST_FAILED"
CELERY_DISABLED = "HOT_OPENING_CELERY_DISABLED"
CELERY_DISPATCH_FAILED = "HOT_OPENING_CELERY_DISPATCH_FAILED"
CELERY_DISPATCH_MARK_FAILED = "HOT_OPENING_CELERY_DISPATCH_MARK_FAILED"
IMAGE_PROMPT_SUBMITTED = "HOT_OPENING_IMAGE_PROMPT_SUBMITTED"
IMAGE_GENERATE_SUBMITTED = "HOT_OPENING_IMAGE_GENERATE_SUBMITTED"
VIDEO_PROMPT_SUBMITTED = "HOT_OPENING_VIDEO_PROMPT_SUBMITTED"
VIDEO_GENERATE_SUBMITTED = "HOT_OPENING_VIDEO_GENERATE_SUBMITTED"
VIDEO_PROMPT_REMOTE_API_STARTED = "HOT_OPENING_VIDEO_PROMPT_REMOTE_API_STARTED"
VIDEO_PROMPT_REMOTE_API_SUCCESS = "HOT_OPENING_VIDEO_PROMPT_REMOTE_API_SUCCESS"
VIDEO_PROMPT_REMOTE_API_FAILED = "HOT_OPENING_VIDEO_PROMPT_REMOTE_API_FAILED"
VIDEO_PROMPT_RESPONSE_PARSE_FAILED = "HOT_OPENING_VIDEO_PROMPT_RESPONSE_PARSE_FAILED"
VIDEO_PROMPT_RESPONSE_EMPTY = "HOT_OPENING_VIDEO_PROMPT_RESPONSE_EMPTY"
class HotOpeningRemoteActionEnum(StrEnum):
"""爆款开头复刻远程模型动作。"""
VIDEO_PROMPT_OPTIMIZE = "video_prompt_optimize"
+51
View File
@@ -90,3 +90,54 @@ class ShotSegmentReplicateStatusEnum(StrEnum):
PROCESSING = "processing" PROCESSING = "processing"
COMPLETED = "completed" COMPLETED = "completed"
FAILED = "failed" FAILED = "failed"
class ShotReplicateLogEventEnum(StrEnum):
"""拆镜复刻模块业务日志事件。"""
API_REQUEST_RECEIVED = "SHOT_API_REQUEST_RECEIVED"
API_REQUEST_SUBMITTED = "SHOT_API_REQUEST_SUBMITTED"
API_REQUEST_REJECTED = "SHOT_API_REQUEST_REJECTED"
API_REQUEST_FAILED = "SHOT_API_REQUEST_FAILED"
CELERY_DISABLED = "SHOT_CELERY_DISABLED"
CELERY_DISPATCH_FAILED = "SHOT_CELERY_DISPATCH_FAILED"
CELERY_DISPATCH_MARK_FAILED = "SHOT_CELERY_DISPATCH_MARK_FAILED"
TASK_SET_CREATED = "SHOT_TASK_SET_CREATED"
TASK_SET_REANALYZE_RECEIVED = "SHOT_TASK_SET_REANALYZE_RECEIVED"
TASK_SET_REANALYZE_SUBMITTED = "SHOT_TASK_SET_REANALYZE_SUBMITTED"
TASK_SET_REANALYZE_REJECTED = "SHOT_TASK_SET_REANALYZE_REJECTED"
TASK_SET_REANALYZE_FAILED = "SHOT_TASK_SET_REANALYZE_FAILED"
ANALYSIS_STARTED = "SHOT_ANALYSIS_STARTED"
ANALYSIS_SUCCESS = "SHOT_ANALYSIS_SUCCESS"
ANALYSIS_FAILED = "SHOT_ANALYSIS_FAILED"
ANALYSIS_REMOTE_API_STARTED = "SHOT_ANALYSIS_REMOTE_API_STARTED"
ANALYSIS_REMOTE_API_SUCCESS = "SHOT_ANALYSIS_REMOTE_API_SUCCESS"
ANALYSIS_REMOTE_API_FAILED = "SHOT_ANALYSIS_REMOTE_API_FAILED"
ANALYSIS_RESPONSE_PARSE_FAILED = "SHOT_ANALYSIS_RESPONSE_PARSE_FAILED"
ANALYSIS_RESPONSE_EMPTY = "SHOT_ANALYSIS_RESPONSE_EMPTY"
SEGMENT_REANALYZE_RECEIVED = "SHOT_SEGMENT_REANALYZE_RECEIVED"
SEGMENT_REANALYZE_SUBMITTED = "SHOT_SEGMENT_REANALYZE_SUBMITTED"
SEGMENT_REANALYZE_REJECTED = "SHOT_SEGMENT_REANALYZE_REJECTED"
SEGMENT_REANALYZE_FAILED = "SHOT_SEGMENT_REANALYZE_FAILED"
SEGMENT_ANALYSIS_STARTED = "SHOT_SEGMENT_ANALYSIS_STARTED"
SEGMENT_ANALYSIS_SUCCESS = "SHOT_SEGMENT_ANALYSIS_SUCCESS"
SEGMENT_ANALYSIS_FAILED = "SHOT_SEGMENT_ANALYSIS_FAILED"
SEGMENT_ANALYSIS_REMOTE_API_STARTED = "SHOT_SEGMENT_ANALYSIS_REMOTE_API_STARTED"
SEGMENT_ANALYSIS_REMOTE_API_SUCCESS = "SHOT_SEGMENT_ANALYSIS_REMOTE_API_SUCCESS"
SEGMENT_ANALYSIS_REMOTE_API_FAILED = "SHOT_SEGMENT_ANALYSIS_REMOTE_API_FAILED"
SPLIT_STATUS_CHANGED = "SHOT_SPLIT_STATUS_CHANGED"
SPLIT_BY_AI_SUBMITTED = "SHOT_SPLIT_BY_AI_SUBMITTED"
SPLIT_CUSTOM_SUBMITTED = "SHOT_SPLIT_CUSTOM_SUBMITTED"
SEGMENT_DELETED = "SHOT_SEGMENT_DELETED"
class ShotReplicateRemoteActionEnum(StrEnum):
"""拆镜复刻远程模型动作。"""
ANALYZE_ORIGINAL_VIDEO = "analyze_original_video"
ANALYZE_CUSTOM_SEGMENT_VIDEO = "analyze_custom_segment_video"
VIDEO_PROMPT_OPTIMIZE = "video_prompt_optimize"
@@ -700,6 +700,28 @@ class ShotSegmentListOut(BaseModel):
items: list[ShotSegmentOut] = Field(default_factory=list, description="拆镜片段列表") items: list[ShotSegmentOut] = Field(default_factory=list, description="拆镜片段列表")
class ShotReanalyzeRequest(BaseModel):
force: bool = Field(False, description="是否强制重跑;默认 false。当前仅允许失败/待处理数据重试,已完成数据不建议强制覆盖")
reason: str | None = Field(None, max_length=200, description="再次分析原因,会写入模块日志")
@field_validator("reason", mode="before")
@classmethod
def _strip_reason(cls, value: str | None) -> str | None:
if value is None:
return None
value = str(value).strip()
return value or None
class ShotReanalyzeOut(BaseModel):
message: str = Field(..., description="操作结果提示")
task_set_id: str | None = Field(None, description="拆镜总任务集ID")
segment_id: str | None = Field(None, description="拆镜片段ID")
analysis_status: str = Field(..., description="重置后的分析状态")
celery_task_name: str = Field(..., description="已投递或待投递的 Celery 任务名")
class ShotSplitByAIOut(BaseModel): class ShotSplitByAIOut(BaseModel):
task_set_id: str = Field(..., description="拆镜总任务集ID") task_set_id: str = Field(..., description="拆镜总任务集ID")
status: str = Field(..., description="总任务状态:pending_analysis/analyzing/analysis_completed/analysis_failed/splitting/split_completed/partial_failed/failed/deleted") status: str = Field(..., description="总任务状态:pending_analysis/analyzing/analysis_completed/analysis_failed/splitting/split_completed/partial_failed/failed/deleted")
@@ -1183,6 +1183,10 @@ async def run_video_prompt_optimize(db: AsyncSession, *, project_id: str, step_i
video_config=video_config, video_config=video_config,
target_platform=target_platform, target_platform=target_platform,
schema_config_snapshot=schema_config_snapshot, schema_config_snapshot=schema_config_snapshot,
module=project.module,
project_id=project.id,
step_id=step.id,
trace_id=f"hot-video-prompt:{step.id}",
) )
billing = await charge_module_prompt_usage( billing = await charge_module_prompt_usage(
db, db,
@@ -10,6 +10,10 @@ from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from app.config import settings from app.config import settings
from app.enums.common import LogEventStatusEnum, LogSourceEnum
from app.enums.hot_opening_replicate import HotOpeningLogEventEnum, HotOpeningRemoteActionEnum, ModuleCodeEnum as HotModuleCodeEnum
from app.enums.shot_replicate import ModuleCodeEnum as ShotModuleCodeEnum, ShotReplicateLogEventEnum, ShotReplicateRemoteActionEnum
from app.services.operation_log_service import log_ai_model_event
from app.enums.common import ( from app.enums.common import (
VIDEO_SCHEMA_CONFIG_DATABASE_SOURCE, VIDEO_SCHEMA_CONFIG_DATABASE_SOURCE,
VIDEO_SCHEMA_CONFIG_DEFAULT_SOURCE, VIDEO_SCHEMA_CONFIG_DEFAULT_SOURCE,
@@ -1415,6 +1419,100 @@ def _mock_result(video_config: dict[str, Any], target_platform: str) -> dict[str
return schema return schema
def _safe_response_json(response: httpx.Response | None) -> Any:
if response is None:
return None
try:
return response.json()
except Exception:
return {"raw_text": response.text}
def _extract_remote_request_id(data: Any) -> str | None:
if isinstance(data, dict):
error = data.get("error") if isinstance(data.get("error"), dict) else {}
request_id = data.get("request_id") or error.get("request_id")
message = error.get("message") or data.get("message")
if not request_id and isinstance(message, str):
import re
match = re.search(r"Request id:\s*([a-zA-Z0-9_.:-]+)", message, flags=re.IGNORECASE)
if match:
request_id = match.group(1)
return str(request_id) if request_id else None
return None
def _video_prompt_remote_event(module: str, *, started: bool = False, success: bool = False, parse_failed: bool = False, empty: bool = False) -> tuple[str, str]:
if module == ShotModuleCodeEnum.SHOT_REPLICATE.value:
action = ShotReplicateRemoteActionEnum.VIDEO_PROMPT_OPTIMIZE.value
if started:
return ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_STARTED.value, action
if success:
return ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_SUCCESS.value, action
if parse_failed:
return ShotReplicateLogEventEnum.ANALYSIS_RESPONSE_PARSE_FAILED.value, action
if empty:
return ShotReplicateLogEventEnum.ANALYSIS_RESPONSE_EMPTY.value, action
return ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_FAILED.value, action
action = HotOpeningRemoteActionEnum.VIDEO_PROMPT_OPTIMIZE.value
if started:
return HotOpeningLogEventEnum.VIDEO_PROMPT_REMOTE_API_STARTED.value, action
if success:
return HotOpeningLogEventEnum.VIDEO_PROMPT_REMOTE_API_SUCCESS.value, action
if parse_failed:
return HotOpeningLogEventEnum.VIDEO_PROMPT_RESPONSE_PARSE_FAILED.value, action
if empty:
return HotOpeningLogEventEnum.VIDEO_PROMPT_RESPONSE_EMPTY.value, action
return HotOpeningLogEventEnum.VIDEO_PROMPT_REMOTE_API_FAILED.value, action
def _log_video_prompt_ai_event(
*,
module: str,
event_type: str,
action: str,
event_status: str,
config: ModelConfig,
trace_id: str | None,
user_id: str | None,
project_id: str | None,
step_id: str | None,
request_data: dict[str, Any] | None = None,
response_data: Any = None,
token_usage: dict[str, Any] | None = None,
http_status: int | None = None,
remote_request_id: str | None = None,
message: str | None = None,
error: str | None = None,
detail: dict[str, Any] | None = None,
) -> None:
log_ai_model_event(
event_type=event_type,
event_status=event_status,
source=LogSourceEnum.REMOTE_API.value,
module=module,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
remote_action=action,
remote_request_id=remote_request_id,
model_config_id=str(config.id),
model_config_name=config.name,
model_name=config.model_name,
provider=config.provider,
api_base=config.api_base,
http_status=http_status,
request=request_data,
response=response_data,
token_usage=token_usage,
message=message,
detail=detail,
error=error,
)
async def _select_model_config(db: AsyncSession) -> ModelConfig | None: async def _select_model_config(db: AsyncSession) -> ModelConfig | None:
result = await db.execute(select(ModelConfig).where(ModelConfig.is_active == True).order_by(ModelConfig.priority.desc()).limit(1)) result = await db.execute(select(ModelConfig).where(ModelConfig.is_active == True).order_by(ModelConfig.priority.desc()).limit(1))
return result.scalar_one_or_none() return result.scalar_one_or_none()
@@ -1432,6 +1530,10 @@ async def optimize_hot_opening_video_prompt(
video_config: dict[str, Any], video_config: dict[str, Any],
target_platform: str = "抖音", target_platform: str = "抖音",
schema_config_snapshot: Any | None = None, schema_config_snapshot: Any | None = None,
module: str = HotModuleCodeEnum.HOT_OPENING_REPLICATE.value,
project_id: str | None = None,
step_id: str | None = None,
trace_id: str | None = None,
) -> tuple[dict[str, Any], str, dict[str, Any]]: ) -> tuple[dict[str, Any], str, dict[str, Any]]:
duration = int(video_config["duration"]) duration = int(video_config["duration"])
references = [ references = [
@@ -1469,18 +1571,109 @@ async def optimize_hot_opening_video_prompt(
"temperature": 0.15, "temperature": 0.15,
"response_format": {"type": "json_object"}, "response_format": {"type": "json_object"},
} }
api_url = f"{config.api_base.rstrip('/')}/chat/completions"
log_request_data = {
**request_data,
"messages": [{"role": "system", "content": build_system_prompt()}, log_user_message],
"model_config_id": config.id,
"model_config_name": config.name,
"provider": config.provider,
"module": module,
"project_id": project_id,
"step_id": step_id,
"material_video_url": material_video_url,
"generated_image_url": generated_image_url,
"target_platform": target_platform,
"api_url": api_url,
}
started_event, remote_action = _video_prompt_remote_event(module, started=True)
_log_video_prompt_ai_event(
module=module,
event_type=started_event,
action=remote_action,
event_status=LogEventStatusEnum.STARTED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
message="视频提词模型请求开始",
)
async with httpx.AsyncClient(timeout=int(settings.CHATAPI_REQUEST_TIMEOUT_SECONDS or 180)) as client: try:
response = await client.post( async with httpx.AsyncClient(timeout=int(settings.CHATAPI_REQUEST_TIMEOUT_SECONDS or 180)) as client:
f"{config.api_base.rstrip('/')}/chat/completions", response = await client.post(
headers={"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}, api_url,
json=request_data, headers={"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"},
json=request_data,
)
except Exception as exc:
failed_event, remote_action = _video_prompt_remote_event(module)
_log_video_prompt_ai_event(
module=module,
event_type=failed_event,
action=remote_action,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
message="视频提词模型请求异常",
error=str(exc),
detail={"exception_type": type(exc).__name__},
) )
raise
response_data = _safe_response_json(response)
remote_request_id = _extract_remote_request_id(response_data)
if response.status_code >= 400: if response.status_code >= 400:
failed_event, remote_action = _video_prompt_remote_event(module)
_log_video_prompt_ai_event(
module=module,
event_type=failed_event,
action=remote_action,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
response_data=response_data,
http_status=response.status_code,
remote_request_id=remote_request_id,
message="视频提词模型请求失败",
error=f"HTTP {response.status_code}: {response.text}",
)
raise RuntimeError(f"视频提词优化失败 HTTP {response.status_code}: {response.text}") raise RuntimeError(f"视频提词优化失败 HTTP {response.status_code}: {response.text}")
data = response.json() try:
content = data["choices"][0]["message"]["content"].strip() data = response.json()
content = data["choices"][0]["message"]["content"].strip()
if not content:
raise RuntimeError("视频提词模型响应 content 为空")
except Exception as exc:
parse_event, remote_action = _video_prompt_remote_event(module, empty="content 为空" in str(exc), parse_failed="content 为空" not in str(exc))
_log_video_prompt_ai_event(
module=module,
event_type=parse_event,
action=remote_action,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
response_data=response_data,
http_status=response.status_code,
remote_request_id=remote_request_id,
message="视频提词模型响应解析失败",
error=str(exc),
)
raise
usage = data.get("usage", {}) or {} usage = data.get("usage", {}) or {}
token_usage = { token_usage = {
"input_tokens": int(usage.get("prompt_tokens") or 0), "input_tokens": int(usage.get("prompt_tokens") or 0),
@@ -1508,8 +1701,47 @@ async def optimize_hot_opening_video_prompt(
"model_name": config.model_name, "model_name": config.model_name,
}) })
result = parse_model_json(content) try:
result = normalize_video_prompt_schema_from_ai(result, video_config, schema_config_snapshot) result = parse_model_json(content)
result = normalize_video_prompt_schema_from_ai(result, video_config, schema_config_snapshot)
except Exception as exc:
parse_event, remote_action = _video_prompt_remote_event(module, parse_failed=True)
_log_video_prompt_ai_event(
module=module,
event_type=parse_event,
action=remote_action,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
response_data=data,
http_status=response.status_code,
remote_request_id=remote_request_id,
message="视频提词业务 JSON 解析失败",
error=str(exc),
)
raise
success_event, remote_action = _video_prompt_remote_event(module, success=True)
_log_video_prompt_ai_event(
module=module,
event_type=success_event,
action=remote_action,
event_status=LogEventStatusEnum.SUCCESS.value,
config=config,
trace_id=trace_id,
user_id=user_id,
project_id=project_id,
step_id=step_id,
request_data=log_request_data,
response_data=data,
token_usage=token_usage,
http_status=response.status_code,
remote_request_id=remote_request_id,
message="视频提词模型请求成功",
)
return result, build_final_video_prompt(result), token_usage return result, build_final_video_prompt(result), token_usage
def _build_file_url_or_data_uri(file_url: str) -> str: def _build_file_url_or_data_uri(file_url: str) -> str:
@@ -98,6 +98,11 @@ def _lease_seconds() -> int:
return max(1, int(settings.MODULE_ASYNC_LEASE_SECONDS or 600)) return max(1, int(settings.MODULE_ASYNC_LEASE_SECONDS or 600))
def _shot_analysis_lease_seconds() -> int:
timeout = max(1, int(getattr(settings, "SHOT_ANALYSIS_TIMEOUT_SECONDS", 600) or 600))
return max(_lease_seconds(), timeout + 120)
def _queue_timeout_seconds() -> int: def _queue_timeout_seconds() -> int:
return max(1, int(settings.MODULE_ASYNC_QUEUE_TIMEOUT_SECONDS or 300)) return max(1, int(settings.MODULE_ASYNC_QUEUE_TIMEOUT_SECONDS or 300))
@@ -227,7 +232,7 @@ async def register_shot_task_set_analysis_task(task_set_id: str) -> str:
module=SHOT_MODULE, module=SHOT_MODULE,
project_id=task_set_id, project_id=task_set_id,
task_set_id=task_set_id, task_set_id=task_set_id,
check_after_seconds=_lease_seconds(), check_after_seconds=_shot_analysis_lease_seconds(),
) )
@@ -242,7 +247,7 @@ async def register_shot_segment_analysis_task(segment_id: str, *, task_set_id: s
project_id=task_set_id, project_id=task_set_id,
task_set_id=task_set_id, task_set_id=task_set_id,
segment_id=segment_id, segment_id=segment_id,
check_after_seconds=_lease_seconds(), check_after_seconds=_shot_analysis_lease_seconds(),
) )
@@ -297,10 +302,15 @@ async def postpone_active_task(
async def mark_active_started(*, object_type: str, object_id: str, reason: str = "started") -> None: async def mark_active_started(*, object_type: str, object_id: str, reason: str = "started") -> None:
delay_seconds = _lease_seconds()
if object_type in {OBJECT_SHOT_TASK_SET_ANALYSIS, OBJECT_SHOT_SEGMENT_ANALYSIS}:
delay_seconds = _shot_analysis_lease_seconds()
elif object_type == OBJECT_SHOT_SPLIT_SEGMENT:
delay_seconds = max(_lease_seconds(), int(settings.SHOT_SPLIT_LEASE_SECONDS or 600))
await postpone_active_task( await postpone_active_task(
object_type=object_type, object_type=object_type,
object_id=object_id, object_id=object_id,
delay_seconds=_lease_seconds(), delay_seconds=delay_seconds,
reason=reason, reason=reason,
) )
@@ -2,7 +2,11 @@ from __future__ import annotations
from typing import Any from typing import Any
from app.services.operation_log_service import build_exception_detail, log_operation_error, log_operation_event from app.enums.common import LogEventStatusEnum, LogSourceEnum
from app.services.operation_log_service import (
build_exception_detail,
log_module_generation_event,
)
def log_module_event_file( def log_module_event_file(
@@ -11,23 +15,28 @@ def log_module_event_file(
event_type: str, event_type: str,
project_id: str | None = None, project_id: str | None = None,
step_id: str | None = None, step_id: str | None = None,
task_id: str | None = None,
user_id: str | None = None, user_id: str | None = None,
trace_id: str | None = None,
source: str | None = None,
event_status: str | None = None,
message: str | None = None, message: str | None = None,
detail: dict[str, Any] | None = None, detail: dict[str, Any] | None = None,
error: str | None = None, error: str | None = None,
) -> None: ) -> None:
log_operation_event( log_module_generation_event(
domain="module_generation",
module=module, module=module,
event_type=event_type, event_type=event_type,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
task_id=task_id,
user_id=user_id, user_id=user_id,
trace_id=trace_id,
message=message, message=message,
detail=detail, detail=detail,
error=error, error=error,
event_status="failed" if error else "success", event_status=event_status or (LogEventStatusEnum.FAILED.value if error else LogEventStatusEnum.SUCCESS.value),
source="service", source=source or LogSourceEnum.SERVICE.value,
) )
@@ -39,23 +48,24 @@ def log_module_prompt_event(
user_id: str, user_id: str,
module: str, module: str,
prompt_type: str, prompt_type: str,
trace_id: str | None = None,
request: dict[str, Any] | None = None, request: dict[str, Any] | None = None,
response: dict[str, Any] | None = None, response: dict[str, Any] | None = None,
token_usage: dict[str, Any] | None = None, token_usage: dict[str, Any] | None = None,
error: str | None = None, error: str | None = None,
) -> None: ) -> None:
log_operation_event( log_module_generation_event(
domain="module_generation",
module=module, module=module,
event_type=event_type, event_type=event_type,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
user_id=user_id, user_id=user_id,
trace_id=trace_id,
message=f"模块 AI 请求:{prompt_type}", message=f"模块 AI 请求:{prompt_type}",
detail={"prompt_type": prompt_type, "request": request or {}, "response": response or {}, "token_usage": token_usage or {}}, detail={"prompt_type": prompt_type, "request": request or {}, "response": response or {}, "token_usage": token_usage or {}},
error=error, error=error,
event_status="failed" if error else "success", event_status=LogEventStatusEnum.FAILED.value if error else LogEventStatusEnum.SUCCESS.value,
source="service", source=LogSourceEnum.SERVICE.value,
) )
@@ -65,22 +75,26 @@ def log_module_error(
event_type: str, event_type: str,
project_id: str | None = None, project_id: str | None = None,
step_id: str | None = None, step_id: str | None = None,
task_id: str | None = None,
user_id: str | None = None, user_id: str | None = None,
trace_id: str | None = None,
source: str | None = None,
message: str | None = None, message: str | None = None,
detail: dict[str, Any] | None = None, detail: dict[str, Any] | None = None,
error: str | None = None, error: str | None = None,
exc: BaseException | None = None, exc: BaseException | None = None,
) -> None: ) -> None:
log_operation_error( log_module_generation_event(
domain="module_generation",
module=module, module=module,
event_type=event_type, event_type=event_type,
project_id=project_id, project_id=project_id,
step_id=step_id, step_id=step_id,
task_id=task_id,
user_id=user_id, user_id=user_id,
trace_id=trace_id,
message=message, message=message,
detail=detail, detail=build_exception_detail(exc, detail),
error=error if error is not None else (str(exc) if exc else None), error=error if error is not None else (str(exc) if exc else None),
exc=exc, event_status=LogEventStatusEnum.FAILED.value,
source="service", source=source or LogSourceEnum.SERVICE.value,
) )
@@ -1,6 +1,7 @@
from __future__ import annotations from __future__ import annotations
import json import json
import logging
import os import os
import re import re
import traceback import traceback
@@ -10,9 +11,14 @@ from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
from app.services.log_config import LOG_DATE_FORMAT, LOG_DIR, is_enabled from app.services.log_config import LOG_DATE_FORMAT, LOG_DIR, is_enabled
logger = logging.getLogger("video_gen")
MAX_LOG_FIELD_LENGTH = 20000 MAX_LOG_FIELD_LENGTH = 20000
MAX_TRACEBACK_LENGTH = 12000 MAX_TRACEBACK_LENGTH = 12000
OPERATION_LOG_ROOT = os.path.join(os.path.dirname(LOG_DIR), "OperationLogs") LOG_BASE_DIR = os.path.dirname(LOG_DIR)
OPERATION_LOG_ROOT = os.path.join(LOG_BASE_DIR, "OperationLogs")
MODULE_GENERATION_LOG_ROOT = os.path.join(LOG_BASE_DIR, "ModuleGeneration")
AI_MODEL_LOG_ROOT = LOG_DIR
SENSITIVE_KEY_PATTERNS = ( SENSITIVE_KEY_PATTERNS = (
"secret", "secret",
"token", "token",
@@ -94,17 +100,69 @@ def build_exception_detail(exc: BaseException | None, extra: dict[str, Any] | No
return detail return detail
def _append_operation_log(domain: str, entry: dict[str, Any]) -> None: def _append_json_log(root_dir: str, domain: str | None, entry: dict[str, Any]) -> None:
if not is_enabled(): if not is_enabled():
return return
try: try:
domain_dir = os.path.join(OPERATION_LOG_ROOT, _safe_name(domain, "default")) target_dir = root_dir if domain is None else os.path.join(root_dir, _safe_name(domain, "default"))
os.makedirs(domain_dir, exist_ok=True) os.makedirs(target_dir, exist_ok=True)
today = datetime.now().strftime(LOG_DATE_FORMAT) today = datetime.now().strftime(LOG_DATE_FORMAT)
with open(os.path.join(domain_dir, f"{today}.log"), "a", encoding="utf-8") as f: with open(os.path.join(target_dir, f"{today}.log"), "a", encoding="utf-8") as f:
f.write(json.dumps(sanitize_log_value(entry), ensure_ascii=False, default=str) + "\n") f.write(json.dumps(sanitize_log_value(entry), ensure_ascii=False, default=str) + "\n")
except Exception: except Exception as exc:
pass logger.warning("operation log write failed: root_dir=%s domain=%s error=%s", root_dir, domain, exc, exc_info=True)
def _append_operation_log(domain: str, entry: dict[str, Any]) -> None:
_append_json_log(OPERATION_LOG_ROOT, domain, entry)
def _base_entry(
*,
log_type: str,
domain: str,
event_type: str,
module: str | None = None,
event_status: str = "success",
source: str | None = None,
trace_id: str | None = None,
request_id: str | None = None,
user_id: str | None = None,
project_id: str | None = None,
session_id: str | None = None,
group_id: str | None = None,
asset_id: str | None = None,
task_id: str | None = None,
step_id: str | None = None,
remote_action: str | None = None,
remote_request_id: str | None = None,
message: str | None = None,
detail: dict[str, Any] | None = None,
error: str | None = None,
) -> dict[str, Any]:
return {
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"log_type": log_type,
"domain": domain,
"module": module or domain,
"event_type": event_type,
"event_status": event_status,
"source": source,
"trace_id": trace_id,
"request_id": request_id,
"user_id": user_id,
"project_id": project_id,
"session_id": session_id,
"group_id": group_id,
"asset_id": asset_id,
"task_id": task_id,
"step_id": step_id,
"remote_action": remote_action,
"remote_request_id": remote_request_id,
"message": message,
"detail": detail or {},
"error": error,
}
def log_operation_event( def log_operation_event(
@@ -131,32 +189,142 @@ def log_operation_event(
) -> None: ) -> None:
_append_operation_log( _append_operation_log(
domain, domain,
{ _base_entry(
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), log_type="operation_event",
"log_type": "operation_event", domain=domain,
"domain": domain, module=module,
"module": module or domain, event_type=event_type,
"event_type": event_type, event_status=event_status,
"event_status": event_status, source=source,
"source": source, trace_id=trace_id,
"trace_id": trace_id, request_id=request_id,
"request_id": request_id, user_id=user_id,
"user_id": user_id, project_id=project_id,
"project_id": project_id, session_id=session_id,
"session_id": session_id, group_id=group_id,
"group_id": group_id, asset_id=asset_id,
"asset_id": asset_id, task_id=task_id,
"task_id": task_id, step_id=step_id,
"step_id": step_id, remote_action=remote_action,
"remote_action": remote_action, remote_request_id=remote_request_id,
"remote_request_id": remote_request_id, message=message,
"message": message, detail=detail,
"detail": detail or {}, error=error,
"error": error, ),
},
) )
def log_module_generation_event(
*,
module: str,
event_type: str,
event_status: str = "success",
source: str | None = None,
trace_id: str | None = None,
request_id: str | None = None,
user_id: str | None = None,
project_id: str | None = None,
task_id: str | None = None,
step_id: str | None = None,
remote_action: str | None = None,
remote_request_id: str | None = None,
message: str | None = None,
detail: dict[str, Any] | None = None,
error: str | None = None,
) -> None:
"""Write module business logs to log/ModuleGeneration/{module}/YYYY-MM-DD.log."""
_append_json_log(
MODULE_GENERATION_LOG_ROOT,
module,
_base_entry(
log_type="module_generation_event",
domain="module_generation",
module=module,
event_type=event_type,
event_status=event_status,
source=source,
trace_id=trace_id,
request_id=request_id,
user_id=user_id,
project_id=project_id,
task_id=task_id,
step_id=step_id,
remote_action=remote_action,
remote_request_id=remote_request_id,
message=message,
detail=detail,
error=error,
),
)
def log_ai_model_event(
*,
event_type: str,
module: str | None = None,
event_status: str = "success",
source: str | None = None,
trace_id: str | None = None,
request_id: str | None = None,
user_id: str | None = None,
project_id: str | None = None,
task_id: str | None = None,
step_id: str | None = None,
remote_action: str | None = None,
remote_request_id: str | None = None,
model_config_id: str | None = None,
model_config_name: str | None = None,
model_name: str | None = None,
provider: str | None = None,
api_base: str | None = None,
http_status: int | None = None,
request: dict[str, Any] | None = None,
response: dict[str, Any] | list[Any] | str | None = None,
token_usage: dict[str, Any] | None = None,
message: str | None = None,
detail: dict[str, Any] | None = None,
error: str | None = None,
) -> None:
"""Write AI model call logs to existing log/AiModel/YYYY-MM-DD.log."""
final_detail = dict(detail or {})
if request is not None:
final_detail["request"] = request
if response is not None:
final_detail["response"] = response
if token_usage is not None:
final_detail["token_usage"] = token_usage
entry = _base_entry(
log_type="ai_model_event",
domain="ai_model",
module=module or "ai_model",
event_type=event_type,
event_status=event_status,
source=source,
trace_id=trace_id,
request_id=request_id,
user_id=user_id,
project_id=project_id,
task_id=task_id,
step_id=step_id,
remote_action=remote_action,
remote_request_id=remote_request_id,
message=message,
detail=final_detail,
error=error,
)
entry.update(
{
"model_name": model_config_name,
"model_id": model_name,
"model_config_id": model_config_id,
"provider": provider,
"api_base": api_base,
"http_status": http_status,
}
)
_append_json_log(AI_MODEL_LOG_ROOT, None, entry)
def log_operation_error(*, domain: str, event_type: str, exc: BaseException | None = None, detail: dict[str, Any] | None = None, **kwargs: Any) -> None: def log_operation_error(*, domain: str, event_type: str, exc: BaseException | None = None, detail: dict[str, Any] | None = None, **kwargs: Any) -> None:
kwargs.setdefault("event_status", "failed") kwargs.setdefault("event_status", "failed")
kwargs["detail"] = build_exception_detail(exc, detail) kwargs["detail"] = build_exception_detail(exc, detail)
@@ -1145,6 +1145,10 @@ async def run_video_prompt_optimize(db: AsyncSession, *, project_id: str, step_i
video_config=video_config, video_config=video_config,
target_platform=target_platform, target_platform=target_platform,
schema_config_snapshot=schema_config_snapshot, schema_config_snapshot=schema_config_snapshot,
module=project.module,
project_id=project.id,
step_id=step.id,
trace_id=f"shot-video-prompt:{step.id}",
) )
billing = await charge_module_prompt_usage( billing = await charge_module_prompt_usage(
db, db,
@@ -15,6 +15,7 @@ from app.enums.shot_replicate import (
ShotAnalysisStatusEnum, ShotAnalysisStatusEnum,
ShotSegmentAnalysisStatusEnum, ShotSegmentAnalysisStatusEnum,
ShotSegmentReplicateStatusEnum, ShotSegmentReplicateStatusEnum,
ShotReplicateLogEventEnum,
ShotSegmentSourceModeEnum, ShotSegmentSourceModeEnum,
ShotSplitStatusEnum, ShotSplitStatusEnum,
ShotTaskSetStatusEnum, ShotTaskSetStatusEnum,
@@ -28,6 +29,7 @@ from app.schemas.shot_replicate import (
ShotSegmentDeleteOut, ShotSegmentDeleteOut,
ShotSegmentDetailOut, ShotSegmentDetailOut,
ShotSegmentListOut, ShotSegmentListOut,
ShotReanalyzeOut,
ShotSegmentOut, ShotSegmentOut,
ShotSplitByAIOut, ShotSplitByAIOut,
ShotSplitByAIRequest, ShotSplitByAIRequest,
@@ -716,3 +718,124 @@ async def delete_segment(
released_size_bytes=int(released_size_bytes or 0), released_size_bytes=int(released_size_bytes or 0),
) )
async def prepare_reanalyze_task_set(
db: AsyncSession,
*,
current_user: User,
task_set_id: str,
force: bool = False,
reason: str | None = None,
) -> ShotReanalyzeOut:
"""重置原视频分析状态,供 API 重新投递 Celery。"""
task_set = await get_task_set_for_user(db, task_set_id=task_set_id, user=current_user, for_update=True)
if task_set.analysis_status == ShotAnalysisStatusEnum.PROCESSING.value:
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.TASK_SET_REANALYZE_REJECTED.value,
project_id=task_set.id,
user_id=task_set.user_id,
message="原视频分析正在处理中,拒绝再次分析",
detail={"task_set_id": task_set.id, "analysis_status": task_set.analysis_status, "reason": reason},
event_status="rejected",
)
raise HTTPException(status_code=409, detail="原视频分析正在处理中,不能重复投递")
if task_set.analysis_status == ShotAnalysisStatusEnum.COMPLETED.value and not force:
raise HTTPException(status_code=409, detail="原视频分析已完成,如确需重跑请传 force=true")
if force:
active_segments_result = await db.execute(
select(func.count())
.select_from(ShotReplicateSegment)
.where(
ShotReplicateSegment.task_set_id == task_set.id,
ShotReplicateSegment.deleted_at.is_(None),
)
)
if int(active_segments_result.scalar() or 0) > 0:
raise HTTPException(status_code=409, detail="当前总任务集已存在拆镜片段,不能强制重跑原视频分析")
task_set.status = ShotTaskSetStatusEnum.PENDING_ANALYSIS.value
task_set.analysis_status = ShotAnalysisStatusEnum.PENDING.value
task_set.analysis_error_message = None
task_set.original_video_content = None
task_set.original_video_category = None
task_set.original_video_audience = None
task_set.ai_suggestion_json = None
task_set.analysis_raw_json = None
task_set.analysis_result_json = None
await db.flush()
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.TASK_SET_REANALYZE_RECEIVED.value,
project_id=task_set.id,
user_id=task_set.user_id,
message="原视频再次分析已重置状态",
detail={"task_set_id": task_set.id, "force": force, "reason": reason, "video_url": task_set.video_url},
)
return ShotReanalyzeOut(
message="原视频再次分析任务已准备投递",
task_set_id=task_set.id,
segment_id=None,
analysis_status=task_set.analysis_status,
celery_task_name="shot_replicate.analyze_original_video",
)
async def prepare_reanalyze_segment(
db: AsyncSession,
*,
current_user: User,
segment_id: str,
force: bool = False,
reason: str | None = None,
) -> ShotReanalyzeOut:
"""重置自定义切片视频分析状态,供 API 重新投递 Celery。"""
segment = await get_segment_for_user(db, segment_id=segment_id, user=current_user, for_update=True)
if segment.split_status != ShotSplitStatusEnum.COMPLETED.value:
raise HTTPException(status_code=409, detail="当前片段还未切割完成,不能再次分析")
if not segment.segment_video_url:
raise HTTPException(status_code=409, detail="当前片段缺少 segment_video_url,不能再次分析")
if segment.analysis_status == ShotSegmentAnalysisStatusEnum.PROCESSING.value:
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.SEGMENT_REANALYZE_REJECTED.value,
project_id=segment.task_set_id,
step_id=segment.id,
user_id=segment.user_id,
message="切片视频分析正在处理中,拒绝再次分析",
detail={"segment_id": segment.id, "analysis_status": segment.analysis_status, "reason": reason},
event_status="rejected",
)
raise HTTPException(status_code=409, detail="切片视频分析正在处理中,不能重复投递")
if segment.analysis_status == ShotSegmentAnalysisStatusEnum.COMPLETED.value and not force:
raise HTTPException(status_code=409, detail="切片视频分析已完成,如确需重跑请传 force=true")
if segment.source_mode != ShotSegmentSourceModeEnum.CUSTOM.value and not force:
raise HTTPException(status_code=409, detail="AI 建议片段默认无需单独分析,如确需重跑请传 force=true")
segment.analysis_status = ShotSegmentAnalysisStatusEnum.PENDING.value
segment.analysis_error_message = None
segment.analysis_json = None
segment.original_video_content = None
segment.original_video_category = None
segment.original_video_audience = None
segment.segment_content = None
segment.segment_category = None
segment.segment_audience = None
await db.flush()
log_module_event_file(
module=MODULE,
event_type=ShotReplicateLogEventEnum.SEGMENT_REANALYZE_RECEIVED.value,
project_id=segment.task_set_id,
step_id=segment.id,
user_id=segment.user_id,
message="切片视频再次分析已重置状态",
detail={"segment_id": segment.id, "task_set_id": segment.task_set_id, "force": force, "reason": reason, "video_url": segment.segment_video_url},
)
return ShotReanalyzeOut(
message="切片视频再次分析任务已准备投递",
task_set_id=segment.task_set_id,
segment_id=segment.id,
analysis_status=segment.analysis_status,
celery_task_name="shot_replicate.analyze_custom_segment_video",
)
@@ -19,6 +19,9 @@ from app.models.token_usage import TokenUsage
from app.services.upload_video_asset_service import resolve_upload_video_path from app.services.upload_video_asset_service import resolve_upload_video_path
from app.services.resource_signed_url_service import build_resource_signed_url from app.services.resource_signed_url_service import build_resource_signed_url
from app.utils.id_gen import generate_id from app.utils.id_gen import generate_id
from app.enums.common import LogEventStatusEnum, LogSourceEnum
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateLogEventEnum, ShotReplicateRemoteActionEnum
from app.services.operation_log_service import log_ai_model_event
AnalysisMode = Literal["full_breakdown", "summary_only"] AnalysisMode = Literal["full_breakdown", "summary_only"]
@@ -74,7 +77,7 @@ def build_file_url_or_data_uri(file_url: str, fallback_mime: str = "video/mp4")
# return f"data:{mime};base64,{b64}" # return f"data:{mime};base64,{b64}"
def build_user_message(user_text: str, video_url: str) -> tuple[dict[str, Any], dict[str, Any]]: def build_user_message(user_text: str, video_url: str) -> tuple[dict[str, Any], dict[str, Any], str]:
real_url = build_file_url_or_data_uri(video_url) real_url = build_file_url_or_data_uri(video_url)
content_parts = [ content_parts = [
{ {
@@ -96,7 +99,7 @@ def build_user_message(user_text: str, video_url: str) -> tuple[dict[str, Any],
}, },
{"type": "text", "text": user_text}, {"type": "text", "text": user_text},
] ]
return {"role": "user", "content": content_parts}, {"role": "user", "content": log_content_parts} return {"role": "user", "content": content_parts}, {"role": "user", "content": log_content_parts}, real_url
def build_video_analysis_system_prompt(*, mode: AnalysisMode) -> str: def build_video_analysis_system_prompt(*, mode: AnalysisMode) -> str:
@@ -420,12 +423,106 @@ def _int_usage(value: Any) -> int:
except Exception: except Exception:
return 0 return 0
def _safe_response_json(response: httpx.Response | None) -> Any:
if response is None:
return None
try:
return response.json()
except Exception:
return {"raw_text": response.text}
def _extract_remote_error(data: Any) -> tuple[str | None, str | None, str | None, str | None]:
"""Return remote_request_id, remote_code, remote_message, remote_param."""
if isinstance(data, dict):
err = data.get("error") if isinstance(data.get("error"), dict) else data
remote_code = err.get("code") or err.get("type") if isinstance(err, dict) else None
remote_message = err.get("message") if isinstance(err, dict) else None
remote_param = err.get("param") if isinstance(err, dict) else None
remote_request_id = err.get("request_id") or data.get("request_id") if isinstance(err, dict) else data.get("request_id")
if not remote_request_id and isinstance(remote_message, str):
match = re.search(r"Request id:\s*([a-zA-Z0-9_.:-]+)", remote_message, flags=re.IGNORECASE)
if match:
remote_request_id = match.group(1)
return (
str(remote_request_id) if remote_request_id else None,
str(remote_code) if remote_code else None,
str(remote_message) if remote_message else None,
str(remote_param) if remote_param else None,
)
return None, None, str(data) if data is not None else None, None
def _log_shot_ai_model_event(
*,
event_type: str,
event_status: str,
config: ModelConfig,
trace_id: str,
user_id: str | None,
task_set_id: str | None,
segment_id: str | None,
mode: AnalysisMode,
request_data: dict[str, Any] | None = None,
response_data: Any = None,
token_usage: dict[str, Any] | None = None,
http_status: int | None = None,
remote_request_id: str | None = None,
remote_code: str | None = None,
remote_message: str | None = None,
remote_param: str | None = None,
message: str | None = None,
error: str | None = None,
extra_detail: dict[str, Any] | None = None,
) -> None:
action = (
ShotReplicateRemoteActionEnum.ANALYZE_ORIGINAL_VIDEO.value
if mode == "full_breakdown"
else ShotReplicateRemoteActionEnum.ANALYZE_CUSTOM_SEGMENT_VIDEO.value
)
detail = dict(extra_detail or {})
detail.update({
"analysis_mode": mode,
"remote_code": remote_code,
"remote_message": remote_message,
"remote_param": remote_param,
})
log_ai_model_event(
event_type=event_type,
event_status=event_status,
source=LogSourceEnum.REMOTE_API.value,
module=ModuleCodeEnum.SHOT_REPLICATE.value,
trace_id=trace_id,
user_id=user_id,
project_id=task_set_id,
step_id=segment_id,
remote_action=action,
remote_request_id=remote_request_id,
model_config_id=str(config.id),
model_config_name=config.name,
model_name=config.model_name,
provider=config.provider,
api_base=config.api_base,
http_status=http_status,
request=request_data,
response=response_data,
token_usage=token_usage,
message=message,
detail=detail,
error=error,
)
async def analyze_video_for_shot_split( async def analyze_video_for_shot_split(
db: AsyncSession, db: AsyncSession,
video_url: str, video_url: str,
*, *,
user_id: str | None = None, user_id: str | None = None,
mode: AnalysisMode = "full_breakdown", mode: AnalysisMode = "full_breakdown",
task_set_id: str | None = None,
segment_id: str | None = None,
trace_id: str | None = None,
) -> ShotVideoAnalysisResult: ) -> ShotVideoAnalysisResult:
"""调用模型完成拆镜/片段分析。 """调用模型完成拆镜/片段分析。
@@ -433,6 +530,7 @@ async def analyze_video_for_shot_split(
不再读取 SHOT_ANALYSIS_API_BASE / SHOT_ANALYSIS_API_KEY / SHOT_ANALYSIS_MODEL_NAME 不再读取 SHOT_ANALYSIS_API_BASE / SHOT_ANALYSIS_API_KEY / SHOT_ANALYSIS_MODEL_NAME
也不再 fallback 到 SEEDANCE_*,避免拆镜分析走错通道。 也不再 fallback 到 SEEDANCE_*,避免拆镜分析走错通道。
""" """
trace_id = trace_id or generate_id()
config = await _select_model_config(db) config = await _select_model_config(db)
if not config: if not config:
raise RuntimeError("拆镜分析模型未配置:请先在 model_configs 表启用可用模型") raise RuntimeError("拆镜分析模型未配置:请先在 model_configs 表启用可用模型")
@@ -445,7 +543,7 @@ async def analyze_video_for_shot_split(
system_prompt = build_video_analysis_system_prompt(mode=mode) system_prompt = build_video_analysis_system_prompt(mode=mode)
user_text = build_video_analysis_user_text(mode=mode) user_text = build_video_analysis_user_text(mode=mode)
user_message, log_user_message = build_user_message(user_text, video_url) user_message, log_user_message, real_video_url = build_user_message(user_text, video_url)
request_data: dict[str, Any] = { request_data: dict[str, Any] = {
"model": config.model_name, "model": config.model_name,
@@ -467,21 +565,128 @@ async def analyze_video_for_shot_split(
"model_config_name": config.name, "model_config_name": config.name,
"provider": config.provider, "provider": config.provider,
"analysis_mode": mode, "analysis_mode": mode,
"video_url": video_url,
"signed_video_url": real_video_url,
"video_fps": _video_fps(),
"timeout_seconds": _timeout_seconds(),
} }
url = f"{str(config.api_base).rstrip('/')}/chat/completions" url = f"{str(config.api_base).rstrip('/')}/chat/completions"
async with httpx.AsyncClient(timeout=_timeout_seconds()) as client: _log_shot_ai_model_event(
response = await client.post( event_type=(
url, ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_STARTED.value
headers={"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"}, if mode == "full_breakdown"
json=request_data, else ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_REMOTE_API_STARTED.value
),
event_status=LogEventStatusEnum.STARTED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
message="拆镜视频分析模型请求开始",
)
try:
async with httpx.AsyncClient(timeout=_timeout_seconds()) as client:
response = await client.post(
url,
headers={"Authorization": f"Bearer {config.api_key}", "Content-Type": "application/json"},
json=request_data,
)
except Exception as exc:
_log_shot_ai_model_event(
event_type=(
ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_FAILED.value
if mode == "full_breakdown"
else ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_REMOTE_API_FAILED.value
),
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
message="拆镜视频分析模型请求异常",
error=str(exc),
extra_detail={"exception_type": type(exc).__name__},
) )
raise
response_data = _safe_response_json(response)
remote_request_id, remote_code, remote_message, remote_param = _extract_remote_error(response_data)
if response.status_code >= 400: if response.status_code >= 400:
_log_shot_ai_model_event(
event_type=(
ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_FAILED.value
if mode == "full_breakdown"
else ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_REMOTE_API_FAILED.value
),
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
response_data=response_data,
http_status=response.status_code,
remote_request_id=remote_request_id,
remote_code=remote_code,
remote_message=remote_message,
remote_param=remote_param,
message="拆镜视频分析模型请求失败",
error=f"HTTP {response.status_code}: {response.text}",
)
raise RuntimeError(f"视频拆镜分析 API 请求失败: HTTP {response.status_code}: {response.text}") raise RuntimeError(f"视频拆镜分析 API 请求失败: HTTP {response.status_code}: {response.text}")
raw = response.json() try:
content = get_message_content_or_raise(raw) raw = response.json()
result = parse_model_json(content) except Exception as exc:
_log_shot_ai_model_event(
event_type=ShotReplicateLogEventEnum.ANALYSIS_RESPONSE_PARSE_FAILED.value,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
response_data={"raw_text": response.text},
http_status=response.status_code,
message="拆镜视频分析模型响应 JSON 解析失败",
error=str(exc),
)
raise
try:
content = get_message_content_or_raise(raw)
result = parse_model_json(content)
except Exception as exc:
event_type = ShotReplicateLogEventEnum.ANALYSIS_RESPONSE_EMPTY.value if "content 为空" in str(exc) else ShotReplicateLogEventEnum.ANALYSIS_RESPONSE_PARSE_FAILED.value
_log_shot_ai_model_event(
event_type=event_type,
event_status=LogEventStatusEnum.FAILED.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
response_data=raw,
http_status=response.status_code,
message="拆镜视频分析模型内容解析失败",
error=str(exc),
)
raise
result = fill_none_with_wu(result) result = fill_none_with_wu(result)
result = ensure_result_schema(result) result = ensure_result_schema(result)
result = filter_and_normalize_breakdown(result, mode=mode) result = filter_and_normalize_breakdown(result, mode=mode)
@@ -500,6 +705,7 @@ async def analyze_video_for_shot_split(
"split_min_seconds": _split_min_seconds(), "split_min_seconds": _split_min_seconds(),
"split_max_seconds": _split_max_seconds(), "split_max_seconds": _split_max_seconds(),
"analysis_mode": mode, "analysis_mode": mode,
"trace_id": trace_id,
"log_request": log_request_data, "log_request": log_request_data,
} }
if not token_usage["total_tokens"]: if not token_usage["total_tokens"]:
@@ -525,5 +731,25 @@ async def analyze_video_for_shot_split(
"model_name": config.model_name, "model_name": config.model_name,
}) })
_log_shot_ai_model_event(
event_type=(
ShotReplicateLogEventEnum.ANALYSIS_REMOTE_API_SUCCESS.value
if mode == "full_breakdown"
else ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_REMOTE_API_SUCCESS.value
),
event_status=LogEventStatusEnum.SUCCESS.value,
config=config,
trace_id=trace_id,
user_id=user_id,
task_set_id=task_set_id,
segment_id=segment_id,
mode=mode,
request_data={**log_request_data, "api_url": url},
response_data=raw,
token_usage=token_usage,
http_status=response.status_code,
message="拆镜视频分析模型请求成功",
)
return ShotVideoAnalysisResult(result=result, raw_response=raw, usage=token_usage) return ShotVideoAnalysisResult(result=result, raw_response=raw, usage=token_usage)
+11 -10
View File
@@ -11,6 +11,7 @@ from app.enums.credit_record import CreditRecordBillingScene, CreditRecordOwnerT
from app.enums.shot_replicate import ( from app.enums.shot_replicate import (
ModuleCodeEnum, ModuleCodeEnum,
ShotAnalysisStatusEnum, ShotAnalysisStatusEnum,
ShotReplicateLogEventEnum,
ShotSegmentAnalysisStatusEnum, ShotSegmentAnalysisStatusEnum,
ShotSegmentSourceModeEnum, ShotSegmentSourceModeEnum,
ShotSplitStatusEnum, ShotSplitStatusEnum,
@@ -113,7 +114,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
log_module_event_file( log_module_event_file(
module=MODULE, module=MODULE,
event_type="SHOT_ANALYSIS_STARTED", event_type=ShotReplicateLogEventEnum.ANALYSIS_STARTED.value,
project_id=task_set_id, project_id=task_set_id,
user_id=task_set_user_id, user_id=task_set_user_id,
message="原视频拆镜分析开始", message="原视频拆镜分析开始",
@@ -121,7 +122,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
) )
async with async_session() as db: async with async_session() as db:
analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=task_set_user_id, mode="full_breakdown") analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=task_set_user_id, mode="full_breakdown", task_set_id=task_set_id, trace_id=f"shot-task-set-analysis:{task_set_id}")
result = await db.execute( result = await db.execute(
select(ShotReplicateTaskSet) select(ShotReplicateTaskSet)
.where(ShotReplicateTaskSet.id == task_set_id, ShotReplicateTaskSet.deleted_at.is_(None)) .where(ShotReplicateTaskSet.id == task_set_id, ShotReplicateTaskSet.deleted_at.is_(None))
@@ -157,7 +158,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id) await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
log_module_prompt_event( log_module_prompt_event(
event_type="SHOT_ANALYSIS_SUCCESS", event_type=ShotReplicateLogEventEnum.ANALYSIS_SUCCESS.value,
project_id=task_set_id, project_id=task_set_id,
step_id=task_set_id, step_id=task_set_id,
user_id=task_set_user_id or "", user_id=task_set_user_id or "",
@@ -169,7 +170,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
) )
log_module_event_file( log_module_event_file(
module=MODULE, module=MODULE,
event_type="SHOT_ANALYSIS_SUCCESS", event_type=ShotReplicateLogEventEnum.ANALYSIS_SUCCESS.value,
project_id=task_set_id, project_id=task_set_id,
user_id=task_set_user_id, user_id=task_set_user_id,
message="原视频拆镜分析成功", message="原视频拆镜分析成功",
@@ -195,7 +196,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id) await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
log_module_error( log_module_error(
module=MODULE, module=MODULE,
event_type="SHOT_ANALYSIS_FAILED", event_type=ShotReplicateLogEventEnum.ANALYSIS_FAILED.value,
project_id=task_set_id, project_id=task_set_id,
user_id=task_set_user_id, user_id=task_set_user_id,
message="原视频拆镜分析失败", message="原视频拆镜分析失败",
@@ -241,7 +242,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
log_module_event_file( log_module_event_file(
module=MODULE, module=MODULE,
event_type="SHOT_SEGMENT_ANALYSIS_STARTED", event_type=ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_STARTED.value,
project_id=task_set_id, project_id=task_set_id,
step_id=segment_id, step_id=segment_id,
user_id=user_id, user_id=user_id,
@@ -250,7 +251,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
) )
async with async_session() as db: async with async_session() as db:
analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=user_id, mode="summary_only") analyzed = await analyze_video_for_shot_split(db, video_url or "", user_id=user_id, mode="summary_only", task_set_id=task_set_id, segment_id=segment_id, trace_id=f"shot-segment-analysis:{segment_id}")
result = await db.execute( result = await db.execute(
select(ShotReplicateSegment) select(ShotReplicateSegment)
.where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None)) .where(ShotReplicateSegment.id == segment_id, ShotReplicateSegment.deleted_at.is_(None))
@@ -287,7 +288,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id) await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
log_module_prompt_event( log_module_prompt_event(
event_type="SHOT_SEGMENT_ANALYSIS_SUCCESS", event_type=ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_SUCCESS.value,
project_id=task_set_id, project_id=task_set_id,
step_id=segment_id, step_id=segment_id,
user_id=user_id or "", user_id=user_id or "",
@@ -299,7 +300,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
) )
log_module_event_file( log_module_event_file(
module=MODULE, module=MODULE,
event_type="SHOT_SEGMENT_ANALYSIS_SUCCESS", event_type=ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_SUCCESS.value,
project_id=task_set_id, project_id=task_set_id,
step_id=segment_id, step_id=segment_id,
user_id=user_id, user_id=user_id,
@@ -326,7 +327,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id) await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
log_module_error( log_module_error(
module=MODULE, module=MODULE,
event_type="SHOT_SEGMENT_ANALYSIS_FAILED", event_type=ShotReplicateLogEventEnum.SEGMENT_ANALYSIS_FAILED.value,
project_id=task_set_id, project_id=task_set_id,
step_id=segment_id, step_id=segment_id,
user_id=user_id, user_id=user_id,