拆镜复刻、爆款开头复刻容灾优化
This commit is contained in:
@@ -8,7 +8,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.hot_opening_replicate import ModuleCodeEnum
|
from app.enums.hot_opening_replicate import HotOpeningStepCodeEnum, ModuleCodeEnum
|
||||||
from app.schemas.hot_opening_replicate import (
|
from app.schemas.hot_opening_replicate import (
|
||||||
HotOpeningActionOut,
|
HotOpeningActionOut,
|
||||||
HotOpeningDeleteOut,
|
HotOpeningDeleteOut,
|
||||||
@@ -40,6 +40,11 @@ from app.services.hot_opening_replicate_service import (
|
|||||||
update_hot_opening_video_prompt_schema,
|
update_hot_opening_video_prompt_schema,
|
||||||
)
|
)
|
||||||
from app.services.module_generation_log_service import log_module_error
|
from app.services.module_generation_log_service import log_module_error
|
||||||
|
from app.services.module_async_recovery_service import (
|
||||||
|
TASK_HOT_IMAGE_PROMPT,
|
||||||
|
TASK_HOT_VIDEO_PROMPT,
|
||||||
|
register_module_step_task,
|
||||||
|
)
|
||||||
from app.tasks.celery_app import celery_app
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
MODULE = ModuleCodeEnum.HOT_OPENING_REPLICATE.value
|
MODULE = ModuleCodeEnum.HOT_OPENING_REPLICATE.value
|
||||||
@@ -431,8 +436,15 @@ async def generate_image_prompt(
|
|||||||
|
|
||||||
from app.tasks.hot_opening_replicate_tasks import start_image_prompt_optimize
|
from app.tasks.hot_opening_replicate_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=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_HOT_IMAGE_PROMPT,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
start_image_prompt_optimize.delay(project_id_value, step_id_value)
|
start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await _mark_dispatch_failed_and_raise(
|
await _mark_dispatch_failed_and_raise(
|
||||||
db,
|
db,
|
||||||
@@ -560,8 +572,15 @@ async def generate_video_prompt(
|
|||||||
|
|
||||||
from app.tasks.hot_opening_replicate_tasks import start_video_prompt_optimize
|
from app.tasks.hot_opening_replicate_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=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_HOT_VIDEO_PROMPT,
|
||||||
|
)
|
||||||
try:
|
try:
|
||||||
start_video_prompt_optimize.delay(project_id_value, step_id_value)
|
start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
await _mark_dispatch_failed_and_raise(
|
await _mark_dispatch_failed_and_raise(
|
||||||
db,
|
db,
|
||||||
|
|||||||
@@ -8,7 +8,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.shot_replicate import ModuleCodeEnum
|
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateStepCodeEnum
|
||||||
from app.schemas.shot_replicate import (
|
from app.schemas.shot_replicate import (
|
||||||
ShotReplicateActionOut,
|
ShotReplicateActionOut,
|
||||||
ShotReplicateDeleteOut,
|
ShotReplicateDeleteOut,
|
||||||
@@ -57,6 +57,14 @@ from app.services.shot_replicate_taskset_service import (
|
|||||||
task_set_detail,
|
task_set_detail,
|
||||||
)
|
)
|
||||||
from app.services.module_generation_log_service import log_module_error, log_module_event_file
|
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
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
MODULE = ModuleCodeEnum.SHOT_REPLICATE.value
|
MODULE = ModuleCodeEnum.SHOT_REPLICATE.value
|
||||||
@@ -141,6 +149,22 @@ def _log_api_exception_from_locals(exc: BaseException, local_values: dict, messa
|
|||||||
exc=exc,
|
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:
|
async def _reload_project_detail(db: AsyncSession, current_user: User, project_id: str) -> ShotReplicateTaskDetailOut:
|
||||||
project = await _get_project_for_user(
|
project = await _get_project_for_user(
|
||||||
db,
|
db,
|
||||||
@@ -213,6 +237,7 @@ async def create_shot_task_set(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
task_set = await create_task_set(db, current_user=current_user, req=req)
|
task_set = await create_task_set(db, current_user=current_user, req=req)
|
||||||
task_set_id = task_set.id
|
task_set_id = task_set.id
|
||||||
@@ -225,13 +250,12 @@ async def create_shot_task_set(
|
|||||||
_log_api_exception_from_locals(exc, locals(), f"创建拆镜总任务集失败: {exc}")
|
_log_api_exception_from_locals(exc, locals(), f"创建拆镜总任务集失败: {exc}")
|
||||||
raise HTTPException(status_code=500, detail=f"创建拆镜总任务集失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"创建拆镜总任务集失败: {exc}")
|
||||||
|
|
||||||
if celery_app:
|
|
||||||
try:
|
try:
|
||||||
from app.tasks.shot_replicate_tasks import analyze_original_video
|
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)
|
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="CELERY_DISPATCH_FAILED",
|
||||||
current_user=current_user,
|
current_user=current_user,
|
||||||
@@ -296,6 +320,7 @@ async def split_by_ai(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
out = await create_segments_by_ai(db, current_user=current_user, task_set_id=task_set_id, req=req)
|
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]
|
segment_ids = [item.id for item in out.segments]
|
||||||
@@ -308,10 +333,10 @@ async def split_by_ai(
|
|||||||
_log_api_exception_from_locals(exc, locals(), f"按 AI 建议拆镜失败: {exc}")
|
_log_api_exception_from_locals(exc, locals(), f"按 AI 建议拆镜失败: {exc}")
|
||||||
raise HTTPException(status_code=500, detail=f"按 AI 建议拆镜失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"按 AI 建议拆镜失败: {exc}")
|
||||||
|
|
||||||
if celery_app:
|
|
||||||
from app.tasks.shot_replicate_tasks import split_one_segment
|
from app.tasks.shot_replicate_tasks import split_one_segment
|
||||||
|
|
||||||
for segment_id in segment_ids:
|
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)
|
split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -327,6 +352,7 @@ async def split_custom(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
out = await create_custom_segment(db, current_user=current_user, task_set_id=task_set_id, req=req)
|
out = await create_custom_segment(db, current_user=current_user, task_set_id=task_set_id, req=req)
|
||||||
segment_id = out.segment.id
|
segment_id = out.segment.id
|
||||||
@@ -339,9 +365,9 @@ async def split_custom(
|
|||||||
_log_api_exception_from_locals(exc, locals(), f"自定义拆镜失败: {exc}")
|
_log_api_exception_from_locals(exc, locals(), f"自定义拆镜失败: {exc}")
|
||||||
raise HTTPException(status_code=500, detail=f"自定义拆镜失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"自定义拆镜失败: {exc}")
|
||||||
|
|
||||||
if celery_app:
|
|
||||||
from app.tasks.shot_replicate_tasks import split_one_segment
|
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)
|
split_one_segment.apply_async(args=[segment_id], queue="gen_result_download", countdown=0)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@@ -520,6 +546,7 @@ async def generate_image_prompt(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
project, step = await submit_image_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
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
|
project_id_value, step_id_value = project.id, step.id
|
||||||
@@ -533,9 +560,15 @@ async def generate_image_prompt(
|
|||||||
raise HTTPException(status_code=500, detail=f"提交图片 AI 提词失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"提交图片 AI 提词失败: {exc}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if celery_app:
|
|
||||||
from app.tasks.shot_replicate_flow_tasks import start_image_prompt_optimize
|
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)
|
start_image_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
except Exception as exc:
|
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}")
|
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}")
|
||||||
@@ -554,6 +587,7 @@ async def generate_image(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
project, step, chat_task = await generate_image_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
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
|
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
||||||
@@ -567,7 +601,6 @@ async def generate_image(
|
|||||||
raise HTTPException(status_code=500, detail=f"提交图片生成失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"提交图片生成失败: {exc}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if celery_app:
|
|
||||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
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)
|
chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
@@ -588,6 +621,7 @@ async def generate_video_prompt(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
project, step = await submit_video_prompt_optimize(db, current_user=current_user, project_id=project_id, req=req)
|
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
|
project_id_value, step_id_value = project.id, step.id
|
||||||
@@ -601,9 +635,15 @@ async def generate_video_prompt(
|
|||||||
raise HTTPException(status_code=500, detail=f"提交视频 AI 提词失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"提交视频 AI 提词失败: {exc}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if celery_app:
|
|
||||||
from app.tasks.shot_replicate_flow_tasks import start_video_prompt_optimize
|
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)
|
start_video_prompt_optimize.apply_async(args=[project_id_value, step_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
except Exception as exc:
|
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}")
|
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}")
|
||||||
@@ -622,6 +662,7 @@ async def generate_video(
|
|||||||
current_user: User = Depends(get_current_user),
|
current_user: User = Depends(get_current_user),
|
||||||
db: AsyncSession = Depends(get_db),
|
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:
|
try:
|
||||||
project, step, chat_task = await generate_video_from_prompt(db, current_user=current_user, project_id=project_id, req=req)
|
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
|
project_id_value, step_id_value, chat_task_id_value = project.id, step.id, chat_task.id
|
||||||
@@ -635,7 +676,6 @@ async def generate_video(
|
|||||||
raise HTTPException(status_code=500, detail=f"提交视频生成失败: {exc}")
|
raise HTTPException(status_code=500, detail=f"提交视频生成失败: {exc}")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if celery_app:
|
|
||||||
from app.tasks.generation_create_tasks import chatapi_create_generation_task
|
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)
|
chatapi_create_generation_task.apply_async(args=[chat_task_id_value], queue="gen_chatapi_create", countdown=0)
|
||||||
|
|||||||
@@ -145,6 +145,17 @@ class Settings(BaseSettings):
|
|||||||
CELERY_STARTUP_RECOVERY_LOCK_KEY: str = "vg:celery:startup_recovery_lock"
|
CELERY_STARTUP_RECOVERY_LOCK_KEY: str = "vg:celery:startup_recovery_lock"
|
||||||
CELERY_STARTUP_RECOVERY_LOCK_TTL_SECONDS: int = 120
|
CELERY_STARTUP_RECOVERY_LOCK_TTL_SECONDS: int = 120
|
||||||
|
|
||||||
|
# 模块异步任务容灾配置。
|
||||||
|
# 覆盖 ModuleGenerationStep 提词任务、shot 原视频/片段分析、shot ffmpeg 切割 active 注册。
|
||||||
|
# 不新增 worker 队列:恢复扫描仍走 gen_result_download,真实业务任务回到原始队列。
|
||||||
|
MODULE_ASYNC_RECOVERY_BATCH_SIZE: int = 100
|
||||||
|
MODULE_ASYNC_QUEUE_TIMEOUT_SECONDS: int = 5 * 60
|
||||||
|
MODULE_ASYNC_LEASE_SECONDS: int = 10 * 60
|
||||||
|
MODULE_ASYNC_LOCK_TTL_SECONDS: int = 10 * 60
|
||||||
|
MODULE_ASYNC_REQUEUE_DELAY_SECONDS: int = 10
|
||||||
|
MODULE_ASYNC_ACTIVE_REDIS_HASH_KEY: str = "vg:celery:module_async:active"
|
||||||
|
MODULE_ASYNC_ACTIVE_REDIS_ZSET_KEY: str = "vg:celery:module_async:active_index"
|
||||||
|
MODULE_ASYNC_LOCK_KEY_PREFIX: str = "vg:celery:module_async:lock"
|
||||||
|
|
||||||
RESOURCE_SIGN_SECRET: str = "resource-signature-secret-key-for-API-authentication"
|
RESOURCE_SIGN_SECRET: str = "resource-signature-secret-key-for-API-authentication"
|
||||||
RESOURCE_SIGN_EXPIRE_SECONDS: int = 60
|
RESOURCE_SIGN_EXPIRE_SECONDS: int = 60
|
||||||
|
|||||||
@@ -0,0 +1,615 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from datetime import datetime, timedelta, timezone
|
||||||
|
from typing import Any, Iterable
|
||||||
|
|
||||||
|
from sqlalchemy import select
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
|
from app.enums.common import ModuleStepStatusEnum
|
||||||
|
from app.enums.hot_opening_replicate import HotOpeningStepCodeEnum, ModuleCodeEnum as HotModuleCodeEnum
|
||||||
|
from app.enums.shot_replicate import (
|
||||||
|
ModuleCodeEnum as ShotModuleCodeEnum,
|
||||||
|
ShotAnalysisStatusEnum,
|
||||||
|
ShotReplicateStepCodeEnum,
|
||||||
|
ShotSegmentAnalysisStatusEnum,
|
||||||
|
ShotSplitStatusEnum,
|
||||||
|
)
|
||||||
|
from app.models.module_generation_step import ModuleGenerationStep
|
||||||
|
from app.models.shot_replicate_segment import ShotReplicateSegment
|
||||||
|
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
|
||||||
|
from app.services.redis_registry_service import (
|
||||||
|
datetime_to_epoch,
|
||||||
|
redis_acquire_lock,
|
||||||
|
redis_get_due_registry_ids,
|
||||||
|
redis_get_registry_payloads,
|
||||||
|
redis_postpone_registry_item,
|
||||||
|
redis_release_lock,
|
||||||
|
redis_remove_registry_item,
|
||||||
|
redis_upsert_registry_item,
|
||||||
|
utc_now,
|
||||||
|
)
|
||||||
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
|
logger = logging.getLogger("video_gen")
|
||||||
|
|
||||||
|
QUEUE_CREATE = "gen_chatapi_create"
|
||||||
|
QUEUE_DOWNLOAD = "gen_result_download"
|
||||||
|
|
||||||
|
OBJECT_MODULE_STEP = "module_step"
|
||||||
|
OBJECT_SHOT_TASK_SET_ANALYSIS = "shot_task_set_analysis"
|
||||||
|
OBJECT_SHOT_SEGMENT_ANALYSIS = "shot_segment_analysis"
|
||||||
|
OBJECT_SHOT_SPLIT_SEGMENT = "shot_split_segment"
|
||||||
|
|
||||||
|
TASK_HOT_IMAGE_PROMPT = "hot_opening.start_image_prompt_optimize"
|
||||||
|
TASK_HOT_VIDEO_PROMPT = "hot_opening.start_video_prompt_optimize"
|
||||||
|
TASK_SHOT_IMAGE_PROMPT = "shot_replicate.start_image_prompt_optimize"
|
||||||
|
TASK_SHOT_VIDEO_PROMPT = "shot_replicate.start_video_prompt_optimize"
|
||||||
|
TASK_SHOT_ANALYZE_ORIGINAL = "shot_replicate.analyze_original_video"
|
||||||
|
TASK_SHOT_ANALYZE_CUSTOM_SEGMENT = "shot_replicate.analyze_custom_segment_video"
|
||||||
|
TASK_SHOT_SPLIT_ONE = "shot_replicate.split_one_segment"
|
||||||
|
|
||||||
|
HOT_MODULE = HotModuleCodeEnum.HOT_OPENING_REPLICATE.value
|
||||||
|
SHOT_MODULE = ShotModuleCodeEnum.SHOT_REPLICATE.value
|
||||||
|
|
||||||
|
TERMINAL_STEP_STATUSES = {
|
||||||
|
ModuleStepStatusEnum.COMPLETED.value,
|
||||||
|
ModuleStepStatusEnum.FAILED.value,
|
||||||
|
ModuleStepStatusEnum.CANCELLED.value,
|
||||||
|
}
|
||||||
|
TERMINAL_ANALYSIS_STATUSES = {
|
||||||
|
ShotAnalysisStatusEnum.COMPLETED.value,
|
||||||
|
ShotAnalysisStatusEnum.FAILED.value,
|
||||||
|
}
|
||||||
|
TERMINAL_SEGMENT_ANALYSIS_STATUSES = {
|
||||||
|
ShotSegmentAnalysisStatusEnum.COMPLETED.value,
|
||||||
|
ShotSegmentAnalysisStatusEnum.FAILED.value,
|
||||||
|
ShotSegmentAnalysisStatusEnum.NOT_REQUIRED.value,
|
||||||
|
}
|
||||||
|
TERMINAL_SPLIT_STATUSES = {
|
||||||
|
ShotSplitStatusEnum.COMPLETED.value,
|
||||||
|
ShotSplitStatusEnum.FAILED.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_aware(value: datetime | None) -> datetime | None:
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if value.tzinfo is None:
|
||||||
|
return value.replace(tzinfo=timezone.utc)
|
||||||
|
return value.astimezone(timezone.utc)
|
||||||
|
|
||||||
|
|
||||||
|
def _hash_key() -> str:
|
||||||
|
return settings.MODULE_ASYNC_ACTIVE_REDIS_HASH_KEY
|
||||||
|
|
||||||
|
|
||||||
|
def _zset_key() -> str:
|
||||||
|
return settings.MODULE_ASYNC_ACTIVE_REDIS_ZSET_KEY
|
||||||
|
|
||||||
|
|
||||||
|
def _lease_seconds() -> int:
|
||||||
|
return max(1, int(settings.MODULE_ASYNC_LEASE_SECONDS or 600))
|
||||||
|
|
||||||
|
|
||||||
|
def _queue_timeout_seconds() -> int:
|
||||||
|
return max(1, int(settings.MODULE_ASYNC_QUEUE_TIMEOUT_SECONDS or 300))
|
||||||
|
|
||||||
|
|
||||||
|
def _requeue_delay_seconds() -> int:
|
||||||
|
return max(1, int(settings.MODULE_ASYNC_REQUEUE_DELAY_SECONDS or 10))
|
||||||
|
|
||||||
|
|
||||||
|
def _item_id(object_type: str, object_id: str) -> str:
|
||||||
|
return f"{object_type}:{object_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _lock_key(object_type: str, object_id: str) -> str:
|
||||||
|
return f"{settings.MODULE_ASYNC_LOCK_KEY_PREFIX}:{object_type}:{object_id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _check_at_after(seconds: int | None = None) -> datetime:
|
||||||
|
return _now() + timedelta(seconds=int(seconds or _lease_seconds()))
|
||||||
|
|
||||||
|
|
||||||
|
def _base_payload(
|
||||||
|
*,
|
||||||
|
object_type: str,
|
||||||
|
object_id: str,
|
||||||
|
task_name: str,
|
||||||
|
queue: str,
|
||||||
|
args: list[Any],
|
||||||
|
module: str | None = None,
|
||||||
|
project_id: str | None = None,
|
||||||
|
step_id: str | None = None,
|
||||||
|
step_code: str | None = None,
|
||||||
|
task_set_id: str | None = None,
|
||||||
|
segment_id: str | None = None,
|
||||||
|
reason: str = "submit",
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
now_epoch = datetime_to_epoch(utc_now())
|
||||||
|
return {
|
||||||
|
"object_type": object_type,
|
||||||
|
"object_id": object_id,
|
||||||
|
"task_name": task_name,
|
||||||
|
"queue": queue,
|
||||||
|
"args": args,
|
||||||
|
"module": module,
|
||||||
|
"project_id": project_id,
|
||||||
|
"step_id": step_id,
|
||||||
|
"step_code": step_code,
|
||||||
|
"task_set_id": task_set_id,
|
||||||
|
"segment_id": segment_id,
|
||||||
|
"reason": reason,
|
||||||
|
"created_at": now_epoch,
|
||||||
|
"updated_at": now_epoch,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def register_active_task(
|
||||||
|
*,
|
||||||
|
object_type: str,
|
||||||
|
object_id: str,
|
||||||
|
task_name: str,
|
||||||
|
queue: str,
|
||||||
|
args: list[Any],
|
||||||
|
module: str | None = None,
|
||||||
|
project_id: str | None = None,
|
||||||
|
step_id: str | None = None,
|
||||||
|
step_code: str | None = None,
|
||||||
|
task_set_id: str | None = None,
|
||||||
|
segment_id: str | None = None,
|
||||||
|
check_after_seconds: int | None = None,
|
||||||
|
reason: str = "submit",
|
||||||
|
) -> str:
|
||||||
|
item_id = _item_id(object_type, object_id)
|
||||||
|
payload = _base_payload(
|
||||||
|
object_type=object_type,
|
||||||
|
object_id=object_id,
|
||||||
|
task_name=task_name,
|
||||||
|
queue=queue,
|
||||||
|
args=args,
|
||||||
|
module=module,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=step_code,
|
||||||
|
task_set_id=task_set_id,
|
||||||
|
segment_id=segment_id,
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
await redis_upsert_registry_item(
|
||||||
|
hash_key=_hash_key(),
|
||||||
|
zset_key=_zset_key(),
|
||||||
|
item_id=item_id,
|
||||||
|
payload=payload,
|
||||||
|
check_at=_check_at_after(check_after_seconds),
|
||||||
|
log_context="module_async_active",
|
||||||
|
)
|
||||||
|
return item_id
|
||||||
|
|
||||||
|
|
||||||
|
async def register_module_step_task(
|
||||||
|
*,
|
||||||
|
module: str,
|
||||||
|
project_id: str,
|
||||||
|
step_id: str,
|
||||||
|
step_code: str,
|
||||||
|
task_name: str,
|
||||||
|
queue: str = QUEUE_CREATE,
|
||||||
|
) -> str:
|
||||||
|
return await register_active_task(
|
||||||
|
object_type=OBJECT_MODULE_STEP,
|
||||||
|
object_id=step_id,
|
||||||
|
task_name=task_name,
|
||||||
|
queue=queue,
|
||||||
|
args=[project_id, step_id],
|
||||||
|
module=module,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=step_code,
|
||||||
|
check_after_seconds=_lease_seconds(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def register_shot_task_set_analysis_task(task_set_id: str) -> str:
|
||||||
|
return await register_active_task(
|
||||||
|
object_type=OBJECT_SHOT_TASK_SET_ANALYSIS,
|
||||||
|
object_id=task_set_id,
|
||||||
|
task_name=TASK_SHOT_ANALYZE_ORIGINAL,
|
||||||
|
queue=QUEUE_CREATE,
|
||||||
|
args=[task_set_id],
|
||||||
|
module=SHOT_MODULE,
|
||||||
|
project_id=task_set_id,
|
||||||
|
task_set_id=task_set_id,
|
||||||
|
check_after_seconds=_lease_seconds(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def register_shot_segment_analysis_task(segment_id: str, *, task_set_id: str | None = None) -> str:
|
||||||
|
return await register_active_task(
|
||||||
|
object_type=OBJECT_SHOT_SEGMENT_ANALYSIS,
|
||||||
|
object_id=segment_id,
|
||||||
|
task_name=TASK_SHOT_ANALYZE_CUSTOM_SEGMENT,
|
||||||
|
queue=QUEUE_CREATE,
|
||||||
|
args=[segment_id],
|
||||||
|
module=SHOT_MODULE,
|
||||||
|
project_id=task_set_id,
|
||||||
|
task_set_id=task_set_id,
|
||||||
|
segment_id=segment_id,
|
||||||
|
check_after_seconds=_lease_seconds(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def register_shot_split_task(segment_id: str, *, task_set_id: str | None = None) -> str:
|
||||||
|
return await register_active_task(
|
||||||
|
object_type=OBJECT_SHOT_SPLIT_SEGMENT,
|
||||||
|
object_id=segment_id,
|
||||||
|
task_name=TASK_SHOT_SPLIT_ONE,
|
||||||
|
queue=QUEUE_DOWNLOAD,
|
||||||
|
args=[segment_id],
|
||||||
|
module=SHOT_MODULE,
|
||||||
|
project_id=task_set_id,
|
||||||
|
task_set_id=task_set_id,
|
||||||
|
segment_id=segment_id,
|
||||||
|
check_after_seconds=max(_lease_seconds(), int(settings.SHOT_SPLIT_LEASE_SECONDS or 600)),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def remove_active_task(*, object_type: str, object_id: str) -> None:
|
||||||
|
await redis_remove_registry_item(
|
||||||
|
hash_key=_hash_key(),
|
||||||
|
zset_key=_zset_key(),
|
||||||
|
item_id=_item_id(object_type, object_id),
|
||||||
|
log_context="module_async_active",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def postpone_active_task(
|
||||||
|
*,
|
||||||
|
object_type: str,
|
||||||
|
object_id: str,
|
||||||
|
delay_seconds: int | None = None,
|
||||||
|
reason: str | None = None,
|
||||||
|
) -> None:
|
||||||
|
item_id = _item_id(object_type, object_id)
|
||||||
|
payloads = await redis_get_registry_payloads(
|
||||||
|
hash_key=_hash_key(),
|
||||||
|
item_ids=[item_id],
|
||||||
|
log_context="module_async_active",
|
||||||
|
)
|
||||||
|
payload = payloads.get(item_id)
|
||||||
|
if payload is not None and reason:
|
||||||
|
payload["reason"] = reason
|
||||||
|
await redis_postpone_registry_item(
|
||||||
|
hash_key=_hash_key(),
|
||||||
|
zset_key=_zset_key(),
|
||||||
|
item_id=item_id,
|
||||||
|
payload=payload,
|
||||||
|
check_at=_check_at_after(delay_seconds or _requeue_delay_seconds()),
|
||||||
|
log_context="module_async_active",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def mark_active_started(*, object_type: str, object_id: str, reason: str = "started") -> None:
|
||||||
|
await postpone_active_task(
|
||||||
|
object_type=object_type,
|
||||||
|
object_id=object_id,
|
||||||
|
delay_seconds=_lease_seconds(),
|
||||||
|
reason=reason,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def acquire_object_lock(*, object_type: str, object_id: str) -> str | None:
|
||||||
|
return await redis_acquire_lock(
|
||||||
|
lock_key=_lock_key(object_type, object_id),
|
||||||
|
ttl_seconds=int(settings.MODULE_ASYNC_LOCK_TTL_SECONDS or _lease_seconds()),
|
||||||
|
log_context="module_async_object_lock",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def release_object_lock(*, object_type: str, object_id: str, token: str | None) -> None:
|
||||||
|
if token:
|
||||||
|
await redis_release_lock(
|
||||||
|
lock_key=_lock_key(object_type, object_id),
|
||||||
|
token=token,
|
||||||
|
log_context="module_async_object_lock",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def cleanup_active_if_terminal(db: AsyncSession, *, object_type: str, object_id: str) -> bool:
|
||||||
|
if object_type == OBJECT_MODULE_STEP:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ModuleGenerationStep)
|
||||||
|
.where(ModuleGenerationStep.id == object_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
step = result.scalar_one_or_none()
|
||||||
|
if not step or step.deleted_at is not None or step.status in TERMINAL_STEP_STATUSES:
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if object_type == OBJECT_SHOT_TASK_SET_ANALYSIS:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateTaskSet)
|
||||||
|
.where(ShotReplicateTaskSet.id == object_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
task_set = result.scalar_one_or_none()
|
||||||
|
if not task_set or task_set.deleted_at is not None or task_set.analysis_status in TERMINAL_ANALYSIS_STATUSES:
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if object_type == OBJECT_SHOT_SEGMENT_ANALYSIS:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateSegment)
|
||||||
|
.where(ShotReplicateSegment.id == object_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
segment = result.scalar_one_or_none()
|
||||||
|
if not segment or segment.deleted_at is not None or segment.analysis_status in TERMINAL_SEGMENT_ANALYSIS_STATUSES:
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
if object_type == OBJECT_SHOT_SPLIT_SEGMENT:
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateSegment)
|
||||||
|
.where(ShotReplicateSegment.id == object_id)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
segment = result.scalar_one_or_none()
|
||||||
|
if not segment or segment.deleted_at is not None or segment.split_status in TERMINAL_SPLIT_STATUSES:
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _payload_args(payload: dict[str, Any]) -> list[Any]:
|
||||||
|
args = payload.get("args")
|
||||||
|
if isinstance(args, list):
|
||||||
|
return args
|
||||||
|
object_type = str(payload.get("object_type") or "")
|
||||||
|
if object_type == OBJECT_MODULE_STEP:
|
||||||
|
return [payload.get("project_id"), payload.get("step_id")]
|
||||||
|
if object_type == OBJECT_SHOT_TASK_SET_ANALYSIS:
|
||||||
|
return [payload.get("task_set_id") or payload.get("object_id")]
|
||||||
|
if object_type in {OBJECT_SHOT_SEGMENT_ANALYSIS, OBJECT_SHOT_SPLIT_SEGMENT}:
|
||||||
|
return [payload.get("segment_id") or payload.get("object_id")]
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def _send_task(task_name: str, *, args: list[Any], queue: str, countdown: int = 0, priority: int | None = None) -> bool:
|
||||||
|
if celery_app is None:
|
||||||
|
return False
|
||||||
|
if not task_name or not queue:
|
||||||
|
return False
|
||||||
|
celery_app.send_task(
|
||||||
|
task_name,
|
||||||
|
args=args,
|
||||||
|
queue=queue,
|
||||||
|
countdown=max(0, int(countdown or 0)),
|
||||||
|
priority=priority if priority is not None else settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def _recover_payload_from_redis(db: AsyncSession, item_id: str, payload: dict[str, Any]) -> str:
|
||||||
|
object_type = str(payload.get("object_type") or "")
|
||||||
|
object_id = str(payload.get("object_id") or "")
|
||||||
|
task_name = str(payload.get("task_name") or "")
|
||||||
|
queue = str(payload.get("queue") or QUEUE_CREATE)
|
||||||
|
args = _payload_args(payload)
|
||||||
|
|
||||||
|
if not object_type or not object_id or not task_name or not args:
|
||||||
|
await redis_remove_registry_item(hash_key=_hash_key(), zset_key=_zset_key(), item_id=item_id, log_context="module_async_active")
|
||||||
|
return "remove_invalid_payload"
|
||||||
|
|
||||||
|
if await cleanup_active_if_terminal(db, object_type=object_type, object_id=object_id):
|
||||||
|
return "remove_terminal"
|
||||||
|
|
||||||
|
if object_type == OBJECT_SHOT_SPLIT_SEGMENT:
|
||||||
|
from app.services.shot_replicate_recovery_service import recover_one_split_segment
|
||||||
|
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateSegment)
|
||||||
|
.where(ShotReplicateSegment.id == object_id, ShotReplicateSegment.deleted_at.is_(None))
|
||||||
|
.with_for_update(skip_locked=True)
|
||||||
|
.limit(1)
|
||||||
|
)
|
||||||
|
segment = result.scalar_one_or_none()
|
||||||
|
if not segment:
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return "remove_missing_split_segment"
|
||||||
|
action = await recover_one_split_segment(db, segment, source="redis_active")
|
||||||
|
if action.startswith("recover_"):
|
||||||
|
await postpone_active_task(object_type=object_type, object_id=object_id, delay_seconds=int(settings.SHOT_SPLIT_LEASE_SECONDS or _lease_seconds()), reason="redis_recovered")
|
||||||
|
elif action.startswith("skip_completed") or action.startswith("skip_failed") or action.startswith("mark_failed"):
|
||||||
|
await remove_active_task(object_type=object_type, object_id=object_id)
|
||||||
|
return f"split_{action}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
_send_task(task_name, args=args, queue=queue, countdown=0, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
||||||
|
await postpone_active_task(object_type=object_type, object_id=object_id, delay_seconds=_lease_seconds(), reason="redis_requeued")
|
||||||
|
return "redis_requeued"
|
||||||
|
except Exception as exc:
|
||||||
|
logger.exception("Redis active 恢复投递失败。item_id=%s, task=%s", item_id, task_name)
|
||||||
|
await postpone_active_task(object_type=object_type, object_id=object_id, delay_seconds=_requeue_delay_seconds(), reason=f"redis_requeue_failed:{exc}")
|
||||||
|
return "redis_requeue_failed"
|
||||||
|
|
||||||
|
|
||||||
|
async def _recover_due_redis_items(db: AsyncSession, *, limit: int) -> dict[str, int]:
|
||||||
|
item_ids = await redis_get_due_registry_ids(
|
||||||
|
zset_key=_zset_key(),
|
||||||
|
limit=limit,
|
||||||
|
now=_now(),
|
||||||
|
log_context="module_async_active",
|
||||||
|
)
|
||||||
|
if not item_ids:
|
||||||
|
return {}
|
||||||
|
payloads = await redis_get_registry_payloads(hash_key=_hash_key(), item_ids=item_ids, log_context="module_async_active")
|
||||||
|
results: dict[str, int] = {}
|
||||||
|
for item_id in item_ids:
|
||||||
|
payload = payloads.get(item_id)
|
||||||
|
if not payload:
|
||||||
|
await redis_remove_registry_item(hash_key=_hash_key(), zset_key=_zset_key(), item_id=item_id, log_context="module_async_active")
|
||||||
|
action = "remove_missing_payload"
|
||||||
|
else:
|
||||||
|
action = await _recover_payload_from_redis(db, item_id, payload)
|
||||||
|
results[action] = results.get(action, 0) + 1
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _is_stale_datetime(value: datetime | None, *, seconds: int, now: datetime) -> bool:
|
||||||
|
checked = _ensure_aware(value)
|
||||||
|
if checked is None:
|
||||||
|
return True
|
||||||
|
return checked + timedelta(seconds=max(1, int(seconds))) <= now
|
||||||
|
|
||||||
|
|
||||||
|
async def _recover_stale_module_steps(db: AsyncSession, *, limit: int) -> dict[str, int]:
|
||||||
|
now = _now()
|
||||||
|
stale_cutoff = now - timedelta(seconds=_lease_seconds())
|
||||||
|
result = await db.execute(
|
||||||
|
select(ModuleGenerationStep)
|
||||||
|
.where(
|
||||||
|
ModuleGenerationStep.deleted_at.is_(None),
|
||||||
|
ModuleGenerationStep.is_current == True,
|
||||||
|
ModuleGenerationStep.status == ModuleStepStatusEnum.PROCESSING.value,
|
||||||
|
ModuleGenerationStep.module.in_([HOT_MODULE, SHOT_MODULE]),
|
||||||
|
ModuleGenerationStep.step_code.in_(
|
||||||
|
[
|
||||||
|
HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||||
|
HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||||
|
ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||||
|
ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||||
|
]
|
||||||
|
),
|
||||||
|
ModuleGenerationStep.updated_at <= stale_cutoff,
|
||||||
|
)
|
||||||
|
.order_by(ModuleGenerationStep.updated_at.asc())
|
||||||
|
.limit(limit)
|
||||||
|
.with_for_update(skip_locked=True)
|
||||||
|
)
|
||||||
|
steps = list(result.scalars().all())
|
||||||
|
results: dict[str, int] = {}
|
||||||
|
for step in steps:
|
||||||
|
if step.module == HOT_MODULE and step.step_code == HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value:
|
||||||
|
task_name = TASK_HOT_IMAGE_PROMPT
|
||||||
|
elif step.module == HOT_MODULE and step.step_code == HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value:
|
||||||
|
task_name = TASK_HOT_VIDEO_PROMPT
|
||||||
|
elif step.module == SHOT_MODULE and step.step_code == ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value:
|
||||||
|
task_name = TASK_SHOT_IMAGE_PROMPT
|
||||||
|
elif step.module == SHOT_MODULE and step.step_code == ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value:
|
||||||
|
task_name = TASK_SHOT_VIDEO_PROMPT
|
||||||
|
else:
|
||||||
|
results["skip_unknown_step"] = results.get("skip_unknown_step", 0) + 1
|
||||||
|
continue
|
||||||
|
|
||||||
|
await register_module_step_task(
|
||||||
|
module=step.module,
|
||||||
|
project_id=step.project_id,
|
||||||
|
step_id=step.id,
|
||||||
|
step_code=step.step_code,
|
||||||
|
task_name=task_name,
|
||||||
|
queue=QUEUE_CREATE,
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
_send_task(task_name, args=[step.project_id, step.id], queue=QUEUE_CREATE, countdown=0, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
||||||
|
results["db_step_requeued"] = results.get("db_step_requeued", 0) + 1
|
||||||
|
except Exception:
|
||||||
|
logger.exception("DB fallback 恢复模块步骤失败。step_id=%s", step.id)
|
||||||
|
results["db_step_requeue_failed"] = results.get("db_step_requeue_failed", 0) + 1
|
||||||
|
await db.commit()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def _recover_stale_shot_task_sets(db: AsyncSession, *, limit: int) -> dict[str, int]:
|
||||||
|
now = _now()
|
||||||
|
stale_cutoff = now - timedelta(seconds=_lease_seconds())
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateTaskSet)
|
||||||
|
.where(
|
||||||
|
ShotReplicateTaskSet.deleted_at.is_(None),
|
||||||
|
ShotReplicateTaskSet.analysis_status.in_([ShotAnalysisStatusEnum.PENDING.value, ShotAnalysisStatusEnum.PROCESSING.value]),
|
||||||
|
ShotReplicateTaskSet.updated_at <= stale_cutoff,
|
||||||
|
)
|
||||||
|
.order_by(ShotReplicateTaskSet.updated_at.asc())
|
||||||
|
.limit(limit)
|
||||||
|
.with_for_update(skip_locked=True)
|
||||||
|
)
|
||||||
|
task_sets = list(result.scalars().all())
|
||||||
|
results: dict[str, int] = {}
|
||||||
|
for task_set in task_sets:
|
||||||
|
await register_shot_task_set_analysis_task(task_set.id)
|
||||||
|
try:
|
||||||
|
_send_task(TASK_SHOT_ANALYZE_ORIGINAL, args=[task_set.id], queue=QUEUE_CREATE, countdown=0, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
||||||
|
results["db_task_set_analysis_requeued"] = results.get("db_task_set_analysis_requeued", 0) + 1
|
||||||
|
except Exception:
|
||||||
|
logger.exception("DB fallback 恢复拆镜原视频分析失败。task_set_id=%s", task_set.id)
|
||||||
|
results["db_task_set_analysis_requeue_failed"] = results.get("db_task_set_analysis_requeue_failed", 0) + 1
|
||||||
|
await db.commit()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
async def _recover_stale_shot_segment_analysis(db: AsyncSession, *, limit: int) -> dict[str, int]:
|
||||||
|
now = _now()
|
||||||
|
stale_cutoff = now - timedelta(seconds=_lease_seconds())
|
||||||
|
result = await db.execute(
|
||||||
|
select(ShotReplicateSegment)
|
||||||
|
.where(
|
||||||
|
ShotReplicateSegment.deleted_at.is_(None),
|
||||||
|
ShotReplicateSegment.segment_video_url.is_not(None),
|
||||||
|
ShotReplicateSegment.analysis_status.in_([ShotSegmentAnalysisStatusEnum.PENDING.value, ShotSegmentAnalysisStatusEnum.PROCESSING.value]),
|
||||||
|
ShotReplicateSegment.updated_at <= stale_cutoff,
|
||||||
|
)
|
||||||
|
.order_by(ShotReplicateSegment.updated_at.asc())
|
||||||
|
.limit(limit)
|
||||||
|
.with_for_update(skip_locked=True)
|
||||||
|
)
|
||||||
|
segments = list(result.scalars().all())
|
||||||
|
results: dict[str, int] = {}
|
||||||
|
for segment in segments:
|
||||||
|
await register_shot_segment_analysis_task(segment.id, task_set_id=segment.task_set_id)
|
||||||
|
try:
|
||||||
|
_send_task(TASK_SHOT_ANALYZE_CUSTOM_SEGMENT, args=[segment.id], queue=QUEUE_CREATE, countdown=0, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
||||||
|
results["db_segment_analysis_requeued"] = results.get("db_segment_analysis_requeued", 0) + 1
|
||||||
|
except Exception:
|
||||||
|
logger.exception("DB fallback 恢复拆镜片段分析失败。segment_id=%s", segment.id)
|
||||||
|
results["db_segment_analysis_requeue_failed"] = results.get("db_segment_analysis_requeue_failed", 0) + 1
|
||||||
|
await db.commit()
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_counts(target: dict[str, int], items: Iterable[tuple[str, int]]) -> None:
|
||||||
|
for key, value in items:
|
||||||
|
target[key] = target.get(key, 0) + int(value)
|
||||||
|
|
||||||
|
|
||||||
|
async def recover_module_async_tasks_once(db: AsyncSession) -> dict[str, Any]:
|
||||||
|
"""统一恢复模块异步任务。\n\n 覆盖范围:\n - hot_opening / shot_replicate 的图片、视频 AI 提词步骤;\n - shot_replicate 原视频分析;\n - shot_replicate 自定义片段分析;\n - shot_replicate split active 注册项。\n\n 拆镜 split 的 DB fallback 仍保留在 shot_replicate_recovery_service,\n 这里主要补 Redis active 恢复和非 split 类任务的 DB fallback。\n """
|
||||||
|
batch_size = max(1, int(settings.MODULE_ASYNC_RECOVERY_BATCH_SIZE or 100))
|
||||||
|
results: dict[str, int] = {}
|
||||||
|
|
||||||
|
redis_results = await _recover_due_redis_items(db, limit=batch_size)
|
||||||
|
_merge_counts(results, redis_results.items())
|
||||||
|
|
||||||
|
step_results = await _recover_stale_module_steps(db, limit=batch_size)
|
||||||
|
_merge_counts(results, step_results.items())
|
||||||
|
|
||||||
|
task_set_results = await _recover_stale_shot_task_sets(db, limit=batch_size)
|
||||||
|
_merge_counts(results, task_set_results.items())
|
||||||
|
|
||||||
|
segment_results = await _recover_stale_shot_segment_analysis(db, limit=batch_size)
|
||||||
|
_merge_counts(results, segment_results.items())
|
||||||
|
|
||||||
|
return {"checked": sum(results.values()), "results": results}
|
||||||
@@ -11,6 +11,7 @@ from app.enums.shot_replicate import ShotSplitStatusEnum
|
|||||||
from app.models.shot_replicate_segment import ShotReplicateSegment
|
from app.models.shot_replicate_segment import ShotReplicateSegment
|
||||||
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
|
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
|
||||||
from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary
|
from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary
|
||||||
|
from app.services.module_async_recovery_service import register_shot_split_task
|
||||||
from app.tasks.celery_app import celery_app
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
|
|
||||||
@@ -81,6 +82,7 @@ async def recover_one_split_segment(db: AsyncSession, segment: ShotReplicateSegm
|
|||||||
await refresh_task_set_split_summary(db, segment.task_set_id)
|
await refresh_task_set_split_summary(db, segment.task_set_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
|
await register_shot_split_task(segment.id, task_set_id=segment.task_set_id)
|
||||||
if celery_app:
|
if celery_app:
|
||||||
split_one_segment.apply_async(
|
split_one_segment.apply_async(
|
||||||
args=[segment.id],
|
args=[segment.id],
|
||||||
|
|||||||
@@ -4,6 +4,10 @@ Celery autodiscover imports ``app.tasks``; importing task modules here ensures
|
|||||||
custom named tasks are registered when workers start.
|
custom named tasks are registered when workers start.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
logger = logging.getLogger("video_gen")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from app.tasks import ( # noqa: F401
|
from app.tasks import ( # noqa: F401
|
||||||
generation_create_tasks,
|
generation_create_tasks,
|
||||||
@@ -12,7 +16,9 @@ try:
|
|||||||
generation_recovery_tasks,
|
generation_recovery_tasks,
|
||||||
hot_opening_replicate_tasks,
|
hot_opening_replicate_tasks,
|
||||||
shot_replicate_tasks,
|
shot_replicate_tasks,
|
||||||
shot_replicate_flow_tasks
|
shot_replicate_flow_tasks,
|
||||||
|
module_async_recovery_tasks,
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
logger.exception("Celery 任务模块导入失败,worker 可能出现 unregistered task。")
|
||||||
|
raise
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ if broker_url:
|
|||||||
"shot_replicate.recover_split_tasks_once": {"queue": "gen_result_download"},
|
"shot_replicate.recover_split_tasks_once": {"queue": "gen_result_download"},
|
||||||
"generation.recover_download_tasks_once": {"queue": "gen_result_download"},
|
"generation.recover_download_tasks_once": {"queue": "gen_result_download"},
|
||||||
"generation.recover_generation_tasks_once": {"queue": "gen_result_download"},
|
"generation.recover_generation_tasks_once": {"queue": "gen_result_download"},
|
||||||
|
"module_async.recover_module_async_tasks_once": {"queue": "gen_result_download"},
|
||||||
"app.tasks.cleanup.*": {"queue": "default"},
|
"app.tasks.cleanup.*": {"queue": "default"},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@@ -109,6 +110,7 @@ def on_worker_ready(sender=None, **kwargs):
|
|||||||
recover_generation_tasks_once,
|
recover_generation_tasks_once,
|
||||||
)
|
)
|
||||||
from app.tasks.shot_replicate_tasks import recover_split_tasks_once
|
from app.tasks.shot_replicate_tasks import recover_split_tasks_once
|
||||||
|
from app.tasks.module_async_recovery_tasks import recover_module_async_tasks_once_task
|
||||||
|
|
||||||
countdown = max(0, int(settings.DOWNLOAD_RECOVERY_STARTUP_DELAY_SECONDS or 0))
|
countdown = max(0, int(settings.DOWNLOAD_RECOVERY_STARTUP_DELAY_SECONDS or 0))
|
||||||
|
|
||||||
@@ -127,6 +129,11 @@ def on_worker_ready(sender=None, **kwargs):
|
|||||||
queue="gen_result_download",
|
queue="gen_result_download",
|
||||||
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
|
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
|
||||||
)
|
)
|
||||||
|
recover_module_async_tasks_once_task.apply_async(
|
||||||
|
countdown=countdown + 15,
|
||||||
|
queue="gen_result_download",
|
||||||
|
priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER,
|
||||||
|
)
|
||||||
|
|
||||||
logger.info("启动容灾恢复任务已投递。countdown=%s", countdown)
|
logger.info("启动容灾恢复任务已投递。countdown=%s", countdown)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
from app.tasks.async_runner import run_async
|
from app.tasks.async_runner import run_async
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Optional
|
from typing import Any, Optional
|
||||||
|
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
|
|
||||||
|
from app.config import settings
|
||||||
from app.models.base import async_session
|
from app.models.base import async_session
|
||||||
from app.models.chat_generation_task import ChatGenerationTask
|
from app.models.chat_generation_task import ChatGenerationTask
|
||||||
from app.services.error_codes import extract_error_message
|
from app.services.error_codes import extract_error_message
|
||||||
@@ -16,6 +17,8 @@ from app.tasks.celery_app import celery_app
|
|||||||
|
|
||||||
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"}
|
ALLOWED_GENERATION_MODES = {"chatapi_async", "hot_opening_replicate", "shot_replicate"}
|
||||||
|
|
||||||
|
def _now() -> datetime:
|
||||||
|
return datetime.now(timezone.utc)
|
||||||
|
|
||||||
def _get_first_value(obj: Any, *field_names: str) -> Optional[Any]:
|
def _get_first_value(obj: Any, *field_names: str) -> Optional[Any]:
|
||||||
"""
|
"""
|
||||||
@@ -242,8 +245,13 @@ async def _run(task_id: str):
|
|||||||
|
|
||||||
await enqueue_download_task(db, task, reason="create_result_ready")
|
await enqueue_download_task(db, task, reason="create_result_ready")
|
||||||
else:
|
else:
|
||||||
from app.tasks.generation_poll_tasks import poll_generation_task
|
from app.tasks.generation_poll_tasks import poll_generation_task, register_poll_active
|
||||||
|
|
||||||
|
await register_poll_active(
|
||||||
|
task,
|
||||||
|
reason="create_provider_success",
|
||||||
|
check_at=_now() + timedelta(seconds=int(settings.POLL_TASK_LEASE_SECONDS or 300)),
|
||||||
|
)
|
||||||
poll_generation_task.apply_async(
|
poll_generation_task.apply_async(
|
||||||
args=[task.id],
|
args=[task.id],
|
||||||
queue="gen_provider_poll",
|
queue="gen_provider_poll",
|
||||||
|
|||||||
@@ -1,45 +1,95 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.enums.hot_opening_replicate import HotOpeningStepCodeEnum, ModuleCodeEnum
|
||||||
from app.models.base import async_session
|
from app.models.base import async_session
|
||||||
from app.services.hot_opening_replicate_service import run_image_prompt_optimize, run_video_prompt_optimize
|
from app.services.hot_opening_replicate_service import run_image_prompt_optimize, run_video_prompt_optimize
|
||||||
|
from app.services.module_async_recovery_service import (
|
||||||
|
OBJECT_MODULE_STEP,
|
||||||
|
TASK_HOT_IMAGE_PROMPT,
|
||||||
|
TASK_HOT_VIDEO_PROMPT,
|
||||||
|
acquire_object_lock,
|
||||||
|
cleanup_active_if_terminal,
|
||||||
|
mark_active_started,
|
||||||
|
release_object_lock,
|
||||||
|
register_module_step_task,
|
||||||
|
)
|
||||||
from app.tasks.async_runner import run_async
|
from app.tasks.async_runner import run_async
|
||||||
from app.tasks.celery_app import celery_app
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
|
MODULE = ModuleCodeEnum.HOT_OPENING_REPLICATE.value
|
||||||
|
|
||||||
|
|
||||||
async def _run_image_prompt(project_id: str, step_id: str | None = None):
|
async def _run_image_prompt(project_id: str, step_id: str | None = None):
|
||||||
|
lock_token: str | None = None
|
||||||
|
if step_id:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
if not lock_token:
|
||||||
|
# 同一个 step 的重复消息正在被其它 worker 处理:当前消息直接忽略,active registry 保留等待真正任务完成或 lease 过期。
|
||||||
|
return None
|
||||||
|
await register_module_step_task(
|
||||||
|
module=MODULE,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=HotOpeningStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_HOT_IMAGE_PROMPT,
|
||||||
|
)
|
||||||
|
await mark_active_started(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
try:
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
result = await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
if step_id:
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
if step_id:
|
||||||
|
await release_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
async def _run_video_prompt(project_id: str, step_id: str | None = None):
|
async def _run_video_prompt(project_id: str, step_id: str | None = None):
|
||||||
|
lock_token: str | None = None
|
||||||
|
if step_id:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
if not lock_token:
|
||||||
|
return None
|
||||||
|
await register_module_step_task(
|
||||||
|
module=MODULE,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=HotOpeningStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_HOT_VIDEO_PROMPT,
|
||||||
|
)
|
||||||
|
await mark_active_started(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
try:
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
result = await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
if step_id:
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
if step_id:
|
||||||
|
await release_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
if celery_app:
|
if celery_app:
|
||||||
@celery_app.task(name="hot_opening.start_image_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30)
|
@celery_app.task(name="hot_opening.start_image_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30)
|
||||||
def start_image_prompt_optimize(self, project_id: str, step_id: str | None = None):
|
def start_image_prompt_optimize(self, project_id: str, step_id: str | None = None):
|
||||||
"""手动触发后的图片 AI 提词任务。
|
"""手动触发后的图片 AI 提词任务。"""
|
||||||
|
|
||||||
该任务路由到现有 gen_chatapi_create 队列,不需要新增 hot_opening worker。
|
|
||||||
"""
|
|
||||||
return run_async(_run_image_prompt(project_id, step_id))
|
return run_async(_run_image_prompt(project_id, step_id))
|
||||||
|
|
||||||
@celery_app.task(name="hot_opening.start_video_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30)
|
@celery_app.task(name="hot_opening.start_video_prompt_optimize", bind=True, max_retries=3, default_retry_delay=30)
|
||||||
def start_video_prompt_optimize(self, project_id: str, step_id: str | None = None):
|
def start_video_prompt_optimize(self, project_id: str, step_id: str | None = None):
|
||||||
"""手动触发后的视频 AI 提词任务。
|
"""手动触发后的视频 AI 提词任务。"""
|
||||||
|
|
||||||
该任务路由到现有 gen_chatapi_create 队列,不需要新增 hot_opening worker。
|
|
||||||
"""
|
|
||||||
return run_async(_run_video_prompt(project_id, step_id))
|
return run_async(_run_video_prompt(project_id, step_id))
|
||||||
else:
|
else:
|
||||||
class _DisabledTask:
|
class _DisabledTask:
|
||||||
def delay(self, *args, **kwargs):
|
def delay(self, *args: Any, **kwargs: Any):
|
||||||
raise RuntimeError("Celery is disabled")
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
def apply_async(self, *args, **kwargs):
|
def apply_async(self, *args: Any, **kwargs: Any):
|
||||||
raise RuntimeError("Celery is disabled")
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
start_image_prompt_optimize = _DisabledTask()
|
start_image_prompt_optimize = _DisabledTask()
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.models.base import async_session
|
||||||
|
from app.services.module_async_recovery_service import recover_module_async_tasks_once
|
||||||
|
from app.tasks.async_runner import run_async
|
||||||
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
|
|
||||||
|
async def _run_recover_module_async_tasks_once() -> dict[str, Any]:
|
||||||
|
async with async_session() as db:
|
||||||
|
return await recover_module_async_tasks_once(db)
|
||||||
|
|
||||||
|
|
||||||
|
if celery_app:
|
||||||
|
|
||||||
|
@celery_app.task(name="module_async.recover_module_async_tasks_once")
|
||||||
|
def recover_module_async_tasks_once_task() -> dict[str, Any]:
|
||||||
|
return run_async(_run_recover_module_async_tasks_once())
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
class _DisabledTask:
|
||||||
|
def delay(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
|
def apply_async(self, *args: Any, **kwargs: Any) -> None:
|
||||||
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
|
recover_module_async_tasks_once_task = _DisabledTask()
|
||||||
@@ -1,21 +1,76 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateStepCodeEnum
|
||||||
from app.models.base import async_session
|
from app.models.base import async_session
|
||||||
|
from app.services.module_async_recovery_service import (
|
||||||
|
OBJECT_MODULE_STEP,
|
||||||
|
TASK_SHOT_IMAGE_PROMPT,
|
||||||
|
TASK_SHOT_VIDEO_PROMPT,
|
||||||
|
acquire_object_lock,
|
||||||
|
cleanup_active_if_terminal,
|
||||||
|
mark_active_started,
|
||||||
|
release_object_lock,
|
||||||
|
register_module_step_task,
|
||||||
|
)
|
||||||
from app.services.shot_replicate_flow_service import run_image_prompt_optimize, run_video_prompt_optimize
|
from app.services.shot_replicate_flow_service import run_image_prompt_optimize, run_video_prompt_optimize
|
||||||
from app.tasks.async_runner import run_async
|
from app.tasks.async_runner import run_async
|
||||||
from app.tasks.celery_app import celery_app
|
from app.tasks.celery_app import celery_app
|
||||||
|
|
||||||
|
MODULE = ModuleCodeEnum.SHOT_REPLICATE.value
|
||||||
|
|
||||||
|
|
||||||
async def _run_image_prompt(project_id: str, step_id: str | None = None):
|
async def _run_image_prompt(project_id: str, step_id: str | None = None):
|
||||||
|
lock_token: str | None = None
|
||||||
|
if step_id:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
if not lock_token:
|
||||||
|
return None
|
||||||
|
await register_module_step_task(
|
||||||
|
module=MODULE,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=ShotReplicateStepCodeEnum.IMAGE_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_SHOT_IMAGE_PROMPT,
|
||||||
|
)
|
||||||
|
await mark_active_started(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
try:
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
result = await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
if step_id:
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
if step_id:
|
||||||
|
await release_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
async def _run_video_prompt(project_id: str, step_id: str | None = None):
|
async def _run_video_prompt(project_id: str, step_id: str | None = None):
|
||||||
|
lock_token: str | None = None
|
||||||
|
if step_id:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
if not lock_token:
|
||||||
|
return None
|
||||||
|
await register_module_step_task(
|
||||||
|
module=MODULE,
|
||||||
|
project_id=project_id,
|
||||||
|
step_id=step_id,
|
||||||
|
step_code=ShotReplicateStepCodeEnum.VIDEO_PROMPT_OPTIMIZE.value,
|
||||||
|
task_name=TASK_SHOT_VIDEO_PROMPT,
|
||||||
|
)
|
||||||
|
await mark_active_started(object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
try:
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
result = await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
if step_id:
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_MODULE_STEP, object_id=step_id)
|
||||||
|
return result
|
||||||
|
finally:
|
||||||
|
if step_id:
|
||||||
|
await release_object_lock(object_type=OBJECT_MODULE_STEP, object_id=step_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
if celery_app:
|
if celery_app:
|
||||||
@@ -32,10 +87,10 @@ if celery_app:
|
|||||||
else:
|
else:
|
||||||
|
|
||||||
class _DisabledTask:
|
class _DisabledTask:
|
||||||
def delay(self, *args, **kwargs):
|
def delay(self, *args: Any, **kwargs: Any):
|
||||||
raise RuntimeError("Celery is disabled")
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
def apply_async(self, *args, **kwargs):
|
def apply_async(self, *args: Any, **kwargs: Any):
|
||||||
raise RuntimeError("Celery is disabled")
|
raise RuntimeError("Celery is disabled")
|
||||||
|
|
||||||
start_image_prompt_optimize = _DisabledTask()
|
start_image_prompt_optimize = _DisabledTask()
|
||||||
|
|||||||
@@ -20,6 +20,20 @@ from app.models.shot_replicate_segment import ShotReplicateSegment
|
|||||||
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
|
from app.models.shot_replicate_task_set import ShotReplicateTaskSet
|
||||||
from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event
|
from app.services.module_generation_log_service import log_module_error, log_module_event_file, log_module_prompt_event
|
||||||
from app.services.redis_registry_service import redis_acquire_lock, redis_release_lock
|
from app.services.redis_registry_service import redis_acquire_lock, redis_release_lock
|
||||||
|
from app.services.module_async_recovery_service import (
|
||||||
|
OBJECT_SHOT_SEGMENT_ANALYSIS,
|
||||||
|
OBJECT_SHOT_SPLIT_SEGMENT,
|
||||||
|
OBJECT_SHOT_TASK_SET_ANALYSIS,
|
||||||
|
acquire_object_lock,
|
||||||
|
cleanup_active_if_terminal,
|
||||||
|
mark_active_started,
|
||||||
|
postpone_active_task,
|
||||||
|
register_shot_segment_analysis_task,
|
||||||
|
register_shot_split_task,
|
||||||
|
register_shot_task_set_analysis_task,
|
||||||
|
release_object_lock,
|
||||||
|
remove_active_task,
|
||||||
|
)
|
||||||
from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary
|
from app.services.shot_replicate_taskset_service import refresh_task_set_split_summary
|
||||||
from app.services.shot_video_analysis_service import analyze_video_for_shot_split
|
from app.services.shot_video_analysis_service import analyze_video_for_shot_split
|
||||||
from app.services.shot_video_split_service import split_video_segment_async
|
from app.services.shot_video_split_service import split_video_segment_async
|
||||||
@@ -65,6 +79,12 @@ async def _release_split_semaphore(lock_key: str | None, segment_id: str) -> Non
|
|||||||
|
|
||||||
|
|
||||||
async def _run_analyze_original_video(task_set_id: str) -> None:
|
async def _run_analyze_original_video(task_set_id: str) -> None:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
|
if not lock_token:
|
||||||
|
return
|
||||||
|
await register_shot_task_set_analysis_task(task_set_id)
|
||||||
|
await mark_active_started(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
|
|
||||||
task_set_user_id: str | None = None
|
task_set_user_id: str | None = None
|
||||||
video_url: str | None = None
|
video_url: str | None = None
|
||||||
try:
|
try:
|
||||||
@@ -77,8 +97,10 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
|
|||||||
)
|
)
|
||||||
task_set = result.scalar_one_or_none()
|
task_set = result.scalar_one_or_none()
|
||||||
if not task_set:
|
if not task_set:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
return
|
return
|
||||||
if task_set.analysis_status == ShotAnalysisStatusEnum.COMPLETED.value:
|
if task_set.analysis_status == ShotAnalysisStatusEnum.COMPLETED.value:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
return
|
return
|
||||||
task_set_user_id = task_set.user_id
|
task_set_user_id = task_set.user_id
|
||||||
video_url = task_set.video_url
|
video_url = task_set.video_url
|
||||||
@@ -107,6 +129,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
|
|||||||
task_set = result.scalar_one_or_none()
|
task_set = result.scalar_one_or_none()
|
||||||
if not task_set:
|
if not task_set:
|
||||||
await db.rollback()
|
await db.rollback()
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
return
|
return
|
||||||
result_json = analyzed.result
|
result_json = analyzed.result
|
||||||
task_set.original_video_content = str(result_json.get("原视频内容") or "无")
|
task_set.original_video_content = str(result_json.get("原视频内容") or "无")
|
||||||
@@ -119,6 +142,7 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
|
|||||||
task_set.status = ShotTaskSetStatusEnum.ANALYSIS_COMPLETED.value
|
task_set.status = ShotTaskSetStatusEnum.ANALYSIS_COMPLETED.value
|
||||||
task_set.analysis_error_message = None
|
task_set.analysis_error_message = None
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
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="SHOT_ANALYSIS_SUCCESS",
|
||||||
@@ -154,6 +178,9 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
|
|||||||
task_set.analysis_status = ShotAnalysisStatusEnum.FAILED.value
|
task_set.analysis_status = ShotAnalysisStatusEnum.FAILED.value
|
||||||
task_set.analysis_error_message = str(exc)
|
task_set.analysis_error_message = str(exc)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
|
||||||
|
else:
|
||||||
|
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="SHOT_ANALYSIS_FAILED",
|
||||||
@@ -163,9 +190,17 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
|
|||||||
detail={"task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "full_breakdown"},
|
detail={"task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "full_breakdown"},
|
||||||
exc=exc,
|
exc=exc,
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
await release_object_lock(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
||||||
|
lock_token = await acquire_object_lock(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
|
if not lock_token:
|
||||||
|
return
|
||||||
|
await register_shot_segment_analysis_task(segment_id)
|
||||||
|
await mark_active_started(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
|
|
||||||
user_id: str | None = None
|
user_id: str | None = None
|
||||||
task_set_id: str | None = None
|
task_set_id: str | None = None
|
||||||
video_url: str | None = None
|
video_url: str | None = None
|
||||||
@@ -179,12 +214,15 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
segment = result.scalar_one_or_none()
|
segment = result.scalar_one_or_none()
|
||||||
if not segment or not segment.segment_video_url:
|
if not segment or not segment.segment_video_url:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
return
|
return
|
||||||
if segment.analysis_status == ShotSegmentAnalysisStatusEnum.COMPLETED.value:
|
if segment.analysis_status == ShotSegmentAnalysisStatusEnum.COMPLETED.value:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
return
|
return
|
||||||
user_id = segment.user_id
|
user_id = segment.user_id
|
||||||
task_set_id = segment.task_set_id
|
task_set_id = segment.task_set_id
|
||||||
video_url = segment.segment_video_url
|
video_url = segment.segment_video_url
|
||||||
|
await register_shot_segment_analysis_task(segment_id, task_set_id=task_set_id)
|
||||||
segment.analysis_status = ShotSegmentAnalysisStatusEnum.PROCESSING.value
|
segment.analysis_status = ShotSegmentAnalysisStatusEnum.PROCESSING.value
|
||||||
segment.analysis_error_message = None
|
segment.analysis_error_message = None
|
||||||
await db.commit()
|
await db.commit()
|
||||||
@@ -210,6 +248,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
|||||||
segment = result.scalar_one_or_none()
|
segment = result.scalar_one_or_none()
|
||||||
if not segment:
|
if not segment:
|
||||||
await db.rollback()
|
await db.rollback()
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
return
|
return
|
||||||
result_json = analyzed.result
|
result_json = analyzed.result
|
||||||
segment.original_video_content = str(result_json.get("原视频内容") or "无")
|
segment.original_video_content = str(result_json.get("原视频内容") or "无")
|
||||||
@@ -222,10 +261,11 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
|||||||
segment.analysis_status = ShotSegmentAnalysisStatusEnum.COMPLETED.value
|
segment.analysis_status = ShotSegmentAnalysisStatusEnum.COMPLETED.value
|
||||||
segment.analysis_error_message = None
|
segment.analysis_error_message = None
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
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="SHOT_SEGMENT_ANALYSIS_SUCCESS",
|
||||||
project_id=task_set_id or segment_id,
|
project_id=task_set_id,
|
||||||
step_id=segment_id,
|
step_id=segment_id,
|
||||||
user_id=user_id or "",
|
user_id=user_id or "",
|
||||||
module=MODULE,
|
module=MODULE,
|
||||||
@@ -258,6 +298,9 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
|||||||
segment.analysis_status = ShotSegmentAnalysisStatusEnum.FAILED.value
|
segment.analysis_status = ShotSegmentAnalysisStatusEnum.FAILED.value
|
||||||
segment.analysis_error_message = str(exc)
|
segment.analysis_error_message = str(exc)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
|
||||||
|
else:
|
||||||
|
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="SHOT_SEGMENT_ANALYSIS_FAILED",
|
||||||
@@ -268,6 +311,8 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
|
|||||||
detail={"segment_id": segment_id, "task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "summary_only"},
|
detail={"segment_id": segment_id, "task_set_id": task_set_id, "video_url": video_url, "analysis_mode": "summary_only"},
|
||||||
exc=exc,
|
exc=exc,
|
||||||
)
|
)
|
||||||
|
finally:
|
||||||
|
await release_object_lock(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id, token=lock_token)
|
||||||
|
|
||||||
|
|
||||||
async def _run_split_one_segment(segment_id: str) -> None:
|
async def _run_split_one_segment(segment_id: str) -> None:
|
||||||
@@ -280,6 +325,9 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
if not segment_lock_token:
|
if not segment_lock_token:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await register_shot_split_task(segment_id)
|
||||||
|
await mark_active_started(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
|
|
||||||
semaphore_key: str | None = None
|
semaphore_key: str | None = None
|
||||||
user_id: str | None = None
|
user_id: str | None = None
|
||||||
task_set_id: str | None = None
|
task_set_id: str | None = None
|
||||||
@@ -294,8 +342,15 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
message="拆镜 ffmpeg 并发闸门已满,稍后重试",
|
message="拆镜 ffmpeg 并发闸门已满,稍后重试",
|
||||||
detail={"segment_id": segment_id, "reason": "semaphore_full"},
|
detail={"segment_id": segment_id, "reason": "semaphore_full"},
|
||||||
)
|
)
|
||||||
|
delay = max(1, int(settings.MODULE_ASYNC_REQUEUE_DELAY_SECONDS or 10))
|
||||||
|
await postpone_active_task(
|
||||||
|
object_type=OBJECT_SHOT_SPLIT_SEGMENT,
|
||||||
|
object_id=segment_id,
|
||||||
|
delay_seconds=delay,
|
||||||
|
reason="semaphore_full",
|
||||||
|
)
|
||||||
if celery_app:
|
if celery_app:
|
||||||
split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=10, priority=settings.DOWNLOAD_TASK_PRIORITY_NORMAL)
|
split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=delay, priority=settings.DOWNLOAD_TASK_PRIORITY_NORMAL)
|
||||||
return
|
return
|
||||||
|
|
||||||
async with async_session() as db:
|
async with async_session() as db:
|
||||||
@@ -307,6 +362,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
segment = result.scalar_one_or_none()
|
segment = result.scalar_one_or_none()
|
||||||
if not segment:
|
if not segment:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
return
|
return
|
||||||
user_id = segment.user_id
|
user_id = segment.user_id
|
||||||
task_set_id = segment.task_set_id
|
task_set_id = segment.task_set_id
|
||||||
@@ -318,8 +374,10 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
task_set = task_set_result.scalar_one_or_none()
|
task_set = task_set_result.scalar_one_or_none()
|
||||||
if not task_set:
|
if not task_set:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
return
|
return
|
||||||
if segment.split_status == ShotSplitStatusEnum.COMPLETED.value and segment.segment_video_url:
|
if segment.split_status == ShotSplitStatusEnum.COMPLETED.value and segment.segment_video_url:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
return
|
return
|
||||||
|
|
||||||
validate_split_range(
|
validate_split_range(
|
||||||
@@ -338,6 +396,8 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
task_set.status = ShotTaskSetStatusEnum.SPLITTING.value
|
task_set.status = ShotTaskSetStatusEnum.SPLITTING.value
|
||||||
task_set.split_status = ShotSplitStatusEnum.PROCESSING.value
|
task_set.split_status = ShotSplitStatusEnum.PROCESSING.value
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
await register_shot_split_task(segment_id, task_set_id=segment.task_set_id)
|
||||||
|
await mark_active_started(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
|
|
||||||
source_path = task_set.video_path
|
source_path = task_set.video_path
|
||||||
date_dir = (segment.created_at or now).strftime("%Y/%m/%d")
|
date_dir = (segment.created_at or now).strftime("%Y/%m/%d")
|
||||||
@@ -379,6 +439,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
segment = result.scalar_one_or_none()
|
segment = result.scalar_one_or_none()
|
||||||
if not segment:
|
if not segment:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
return
|
return
|
||||||
segment.segment_video_url = split_result.url
|
segment.segment_video_url = split_result.url
|
||||||
segment.segment_video_path = split_result.path
|
segment.segment_video_path = split_result.path
|
||||||
@@ -389,6 +450,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
segment.split_last_error = None
|
segment.split_last_error = None
|
||||||
await refresh_task_set_split_summary(db, segment.task_set_id)
|
await refresh_task_set_split_summary(db, segment.task_set_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
|
|
||||||
log_module_event_file(
|
log_module_event_file(
|
||||||
module=MODULE,
|
module=MODULE,
|
||||||
@@ -407,6 +469,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
|
|
||||||
if segment.source_mode == ShotSegmentSourceModeEnum.CUSTOM.value and celery_app:
|
if segment.source_mode == ShotSegmentSourceModeEnum.CUSTOM.value and celery_app:
|
||||||
|
await register_shot_segment_analysis_task(segment.id, task_set_id=segment.task_set_id)
|
||||||
analyze_custom_segment_video.apply_async(args=[segment.id], queue=ANALYSIS_QUEUE, countdown=0)
|
analyze_custom_segment_video.apply_async(args=[segment.id], queue=ANALYSIS_QUEUE, countdown=0)
|
||||||
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -421,6 +484,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
)
|
)
|
||||||
segment = result.scalar_one_or_none()
|
segment = result.scalar_one_or_none()
|
||||||
if not segment:
|
if not segment:
|
||||||
|
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
return
|
return
|
||||||
user_id = user_id or segment.user_id
|
user_id = user_id or segment.user_id
|
||||||
task_set_id = task_set_id or segment.task_set_id
|
task_set_id = task_set_id or segment.task_set_id
|
||||||
@@ -437,9 +501,18 @@ async def _run_split_one_segment(segment_id: str) -> None:
|
|||||||
await refresh_task_set_split_summary(db, segment.task_set_id)
|
await refresh_task_set_split_summary(db, segment.task_set_id)
|
||||||
await db.commit()
|
await db.commit()
|
||||||
|
|
||||||
if segment.split_status == ShotSplitStatusEnum.RETRY_WAITING.value and celery_app:
|
if segment.split_status == ShotSplitStatusEnum.RETRY_WAITING.value:
|
||||||
next_retry_delay = max(1, int(((segment.split_next_retry_at or _now()) - _now()).total_seconds()))
|
next_retry_delay = max(1, int(((segment.split_next_retry_at or _now()) - _now()).total_seconds()))
|
||||||
|
await postpone_active_task(
|
||||||
|
object_type=OBJECT_SHOT_SPLIT_SEGMENT,
|
||||||
|
object_id=segment_id,
|
||||||
|
delay_seconds=next_retry_delay,
|
||||||
|
reason="split_retry_waiting",
|
||||||
|
)
|
||||||
|
if celery_app:
|
||||||
split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=next_retry_delay, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=next_retry_delay, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
|
||||||
|
elif final_failed:
|
||||||
|
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
|
||||||
|
|
||||||
log_module_error(
|
log_module_error(
|
||||||
module=MODULE,
|
module=MODULE,
|
||||||
|
|||||||
Reference in New Issue
Block a user