拆镜复刻、爆款开头复刻容灾优化

This commit is contained in:
2026-06-15 14:41:14 +08:00
parent 9e98c9c981
commit b6b4039f65
12 changed files with 987 additions and 70 deletions
+8 -2
View File
@@ -4,6 +4,10 @@ Celery autodiscover imports ``app.tasks``; importing task modules here ensures
custom named tasks are registered when workers start.
"""
import logging
logger = logging.getLogger("video_gen")
try:
from app.tasks import ( # noqa: F401
generation_create_tasks,
@@ -12,7 +16,9 @@ try:
generation_recovery_tasks,
hot_opening_replicate_tasks,
shot_replicate_tasks,
shot_replicate_flow_tasks
shot_replicate_flow_tasks,
module_async_recovery_tasks,
)
except Exception:
pass
logger.exception("Celery 任务模块导入失败,worker 可能出现 unregistered task。")
raise
+7
View File
@@ -59,6 +59,7 @@ if broker_url:
"shot_replicate.recover_split_tasks_once": {"queue": "gen_result_download"},
"generation.recover_download_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"},
},
)
@@ -109,6 +110,7 @@ def on_worker_ready(sender=None, **kwargs):
recover_generation_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))
@@ -127,6 +129,11 @@ def on_worker_ready(sender=None, **kwargs):
queue="gen_result_download",
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)
except Exception:
@@ -1,10 +1,11 @@
from app.tasks.async_runner import run_async
import json
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from typing import Any, Optional
from sqlalchemy import select
from app.config import settings
from app.models.base import async_session
from app.models.chat_generation_task import ChatGenerationTask
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"}
def _now() -> datetime:
return datetime.now(timezone.utc)
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")
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(
args=[task.id],
queue="gen_provider_poll",
@@ -1,45 +1,95 @@
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.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.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 with async_session() as db:
await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
await db.commit()
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:
result = await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
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 with async_session() as db:
await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
await db.commit()
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:
result = await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
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:
@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):
"""手动触发后的图片 AI 提词任务。
该任务路由到现有 gen_chatapi_create 队列,不需要新增 hot_opening worker。
"""
"""手动触发后的图片 AI 提词任务。"""
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)
def start_video_prompt_optimize(self, project_id: str, step_id: str | None = None):
"""手动触发后的视频 AI 提词任务。
该任务路由到现有 gen_chatapi_create 队列,不需要新增 hot_opening worker。
"""
"""手动触发后的视频 AI 提词任务。"""
return run_async(_run_video_prompt(project_id, step_id))
else:
class _DisabledTask:
def delay(self, *args, **kwargs):
def delay(self, *args: Any, **kwargs: Any):
raise RuntimeError("Celery is disabled")
def apply_async(self, *args, **kwargs):
def apply_async(self, *args: Any, **kwargs: Any):
raise RuntimeError("Celery is disabled")
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 typing import Any
from app.enums.shot_replicate import ModuleCodeEnum, ShotReplicateStepCodeEnum
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.tasks.async_runner import run_async
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 with async_session() as db:
await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
await db.commit()
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:
result = await run_image_prompt_optimize(db, project_id=project_id, step_id=step_id)
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 with async_session() as db:
await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
await db.commit()
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:
result = await run_video_prompt_optimize(db, project_id=project_id, step_id=step_id)
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:
@@ -32,10 +87,10 @@ if celery_app:
else:
class _DisabledTask:
def delay(self, *args, **kwargs):
def delay(self, *args: Any, **kwargs: Any):
raise RuntimeError("Celery is disabled")
def apply_async(self, *args, **kwargs):
def apply_async(self, *args: Any, **kwargs: Any):
raise RuntimeError("Celery is disabled")
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.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.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_video_analysis_service import analyze_video_for_shot_split
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:
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
video_url: str | None = None
try:
@@ -77,8 +97,10 @@ async def _run_analyze_original_video(task_set_id: str) -> None:
)
task_set = result.scalar_one_or_none()
if not task_set:
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
return
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
task_set_user_id = task_set.user_id
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()
if not task_set:
await db.rollback()
await remove_active_task(object_type=OBJECT_SHOT_TASK_SET_ANALYSIS, object_id=task_set_id)
return
result_json = analyzed.result
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.analysis_error_message = None
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(
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_error_message = str(exc)
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(
module=MODULE,
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"},
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:
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
task_set_id: 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()
if not segment or not segment.segment_video_url:
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
return
if segment.analysis_status == ShotSegmentAnalysisStatusEnum.COMPLETED.value:
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
return
user_id = segment.user_id
task_set_id = segment.task_set_id
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_error_message = None
await db.commit()
@@ -210,6 +248,7 @@ async def _run_analyze_custom_segment_video(segment_id: str) -> None:
segment = result.scalar_one_or_none()
if not segment:
await db.rollback()
await remove_active_task(object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
return
result_json = analyzed.result
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_error_message = None
await db.commit()
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SEGMENT_ANALYSIS, object_id=segment_id)
log_module_prompt_event(
event_type="SHOT_SEGMENT_ANALYSIS_SUCCESS",
project_id=task_set_id or segment_id,
project_id=task_set_id,
step_id=segment_id,
user_id=user_id or "",
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_error_message = str(exc)
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(
module=MODULE,
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"},
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:
@@ -280,6 +325,9 @@ async def _run_split_one_segment(segment_id: str) -> None:
if not segment_lock_token:
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
user_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 并发闸门已满,稍后重试",
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:
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
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()
if not segment:
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
return
user_id = segment.user_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()
if not task_set:
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
return
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
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.split_status = ShotSplitStatusEnum.PROCESSING.value
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
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()
if not segment:
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
return
segment.segment_video_url = split_result.url
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
await refresh_task_set_split_summary(db, segment.task_set_id)
await db.commit()
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
log_module_event_file(
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:
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)
except Exception as exc:
@@ -421,6 +484,7 @@ async def _run_split_one_segment(segment_id: str) -> None:
)
segment = result.scalar_one_or_none()
if not segment:
await remove_active_task(object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
return
user_id = user_id or segment.user_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 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()))
split_one_segment.apply_async(args=[segment_id], queue=SPLIT_QUEUE, countdown=next_retry_delay, priority=settings.DOWNLOAD_TASK_PRIORITY_RECOVER)
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)
elif final_failed:
await cleanup_active_if_terminal(db, object_type=OBJECT_SHOT_SPLIT_SEGMENT, object_id=segment_id)
log_module_error(
module=MODULE,