实现 video batch 自动清理与按需标注视频,并补充子进程调用测试。

batch 完成后仅保留数据库文本结果,勾选时才生成临时标注视频(24h TTL);新增 FastAPI 到 reference bundle 与 algorithm_runner 的单元测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin
2026-05-21 16:30:48 +08:00
parent e6434d0bb6
commit 09885b4184
10 changed files with 2047 additions and 1354 deletions

View File

@@ -21,7 +21,14 @@ from app.schemas import SurgeryApiResponse, SurgeryStartRequest
from app.or_site_config import merge_video_rtsp_urls_into_file
from app.services.synthetic_rtsp import StreamSpec, SyntheticRtspManager
from app.services.surgery_pipeline import SurgeryPipeline
from app.services.video_batch_runner import VideoBatchRunner, VideoBatchRunResult
from app.baked import pipeline as bp
from app.services.video_batch_cleanup import (
purge_batch_artifacts,
purge_expired_visualizations,
purge_surgery_batch_tree,
stage_visualization_pending,
)
from app.services.video_batch_runner import VideoBatchRunner
from app.services.voice_terminal_hub import (
VoiceTerminalHub,
assign_voice_terminal_after_recording_started,
@@ -31,25 +38,29 @@ from app.surgery_errors import SurgeryPipelineError
router = APIRouter(prefix="/internal/demo", tags=["demo"])
# Bumped when video-batch flow changes; grep this string in logs after restart to confirm new code.
VIDEO_BATCH_FLOW_MARKER = "early-save+background-vis-v3"
VIDEO_BATCH_FLOW_MARKER = "purge-all+opt-in-vis-v4"
def _background_finalize_visualization(
runner: VideoBatchRunner,
result: VideoBatchRunResult,
surgery_id: str,
) -> None:
try:
runner.finalize_visualization(result, surgery_id=surgery_id)
runner.finalize_visualization(surgery_id=surgery_id)
except Exception:
logger.exception("video batch background visualization failed surgery_id={}", surgery_id)
finally:
purge_expired_visualizations(
runner.root_dir,
ttl_hours=float(bp.VIDEO_BATCH_VIS_TTL_HOURS),
)
class VideoBatchSurgeryResponse(BaseModel):
surgery_id: str
status: str
message: str
visualization_url: str
visualization_url: str | None = None
doctor_name: str | None = None
doctor_id: str | None = None
doctor_display: str | None = None
@@ -72,7 +83,7 @@ def _orchestrate_write_rtsp_host() -> str:
summary="非实时精确模式:上传单路 MP4 并跑配置引用包 batch",
description=(
"仅当 DEMO_ORCHESTRATOR_ENABLED=true。保存上传视频调用配置算法子进程包 main.py默认 algorithm_subprocesses/5.15"
"解析 TSV 后写入最终结果,并调用 visualize_result_video.py 生成带标签视频。"
"解析 TSV 后写入最终结果;可选 include_visualization 生成临时标注视频。"
),
)
async def video_batch_surgery(
@@ -80,6 +91,7 @@ async def video_batch_surgery(
surgery_id: Annotated[str, Form()],
video1: Annotated[UploadFile, File(description="单路完整 MP4")],
candidate_consumables_json: Annotated[str, Form()] = "[]",
include_visualization: Annotated[bool, Form()] = False,
pipeline: SurgeryPipeline = Depends(get_surgery_pipeline),
) -> SurgeryApiResponse:
if len(surgery_id) != 6 or not surgery_id.isdigit():
@@ -113,9 +125,10 @@ async def video_batch_surgery(
detail="video1 is empty",
)
logger.info(
"video batch request surgery_id={} flow={}",
"video batch request surgery_id={} flow={} include_visualization={}",
surgery_id,
VIDEO_BATCH_FLOW_MARKER,
include_visualization,
)
runner = VideoBatchRunner()
suffix = Path(video1.filename or "video.mp4").suffix or ".mp4"
@@ -155,13 +168,34 @@ async def video_batch_surgery(
surgery_id,
)
background_tasks.add_task(_background_finalize_visualization, runner, result, surgery_id)
visualization_url = f"/internal/demo/video-batch-surgery/{surgery_id}/visualization"
cache_input = result.output_path.parent.parent / "input" / "input.mp4"
if include_visualization:
stage_visualization_pending(
runner.root_dir,
surgery_id,
source_mp4=cache_input if cache_input.is_file() else result.input_path,
result_tsv=result.output_path,
)
background_tasks.add_task(_background_finalize_visualization, runner, surgery_id)
purge_batch_artifacts(
runner.root_dir,
surgery_id,
digest=result.video_sha256,
candidate_key=result.candidate_cache_key,
)
purge_surgery_batch_tree(runner.root_dir, surgery_id)
visualization_url: str | None = None
if include_visualization:
visualization_url = f"/internal/demo/video-batch-surgery/{surgery_id}/visualization"
doctor = result.doctor
doctor_suffix = ""
if doctor is not None and doctor.display:
doctor_suffix = f";医生={doctor.display}"
vis_suffix = ";标注视频后台生成中(完成后刷新 visualization URL"
vis_suffix = ""
if include_visualization:
vis_suffix = ";标注视频后台生成中(完成后刷新 visualization URL24 小时内有效)"
return VideoBatchSurgeryResponse(
surgery_id=surgery_id,
status="accepted",