feat: 配置写死与 baked 模块,Alembic 建表,百度仅 BAIDU_*

- 新增 app/baked/algorithm|pipeline,非部署参数不再走 env;Settings 保留 DB/HTTP/RTSP/海康/百度/MinIO/Demo
- 移除 init_db_schema 与 reload 配置;main 仅 check_database;start*.sh 在 uvicorn 前执行 alembic upgrade head
- 依赖 psycopg[binary] 供 Alembic 同步 URL;alembic/env 注释与预发清单更新
- 撕段门控消费管线、各视频/语音/归档调用改为 baked
- 百度环境变量仅 BAIDU_APP_ID、BAIDU_API_KEY、BAIDU_SECRET_KEY 与 BAIDU_* 超时/ASR;人脸脚本与 baidu_speech 文案同步
- 全量单测与 .env.example 更新;.gitignore 忽略 refs/(本地权重/视频不入库)

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-24 15:33:22 +08:00
parent b651364877
commit 8a4bad99d3
47 changed files with 1333 additions and 648 deletions

View File

@@ -2,9 +2,13 @@ from __future__ import annotations
import asyncio
import time
from datetime import datetime, timezone
from pathlib import Path
from loguru import logger
from app.baked import algorithm as ba
from app.baked import pipeline as bp
from app.config import Settings
from sqlalchemy.ext.asyncio import async_sessionmaker
@@ -27,6 +31,15 @@ from app.services.video.session_registry import (
SurgerySessionRegistry,
SurgerySessionState,
)
from app.services.tear_gated_segment_consumption.product_map import (
load_tear_segment_name_to_id,
resolve_tear_segment_labels_yaml_path,
)
from app.services.tear_gated_segment_consumption.report import write_tear_segment_txt
from app.services.tear_gated_segment_consumption.runner import (
TearGatedSegmentModelBundle,
TearGatedSegmentRunner,
)
from app.services.video.stream_worker import CameraStreamWorker, redact_rtsp_url
from app.services.video.types import VideoBackendKind
from app.schemas import SurgeryConsumptionDetail, build_consumption_summary
@@ -69,21 +82,21 @@ class CameraSessionManager:
session_factory: async_sessionmaker | None = None,
registry: SurgerySessionRegistry | None = None,
archive_persister: ArchivePersister | None = None,
tear_segment_models: TearGatedSegmentModelBundle | None = None,
) -> None:
self._s = settings
self._vision = vision_algorithm
self._hik = hikvision_runtime
self._tear_models = tear_segment_models
self._session_factory: async_sessionmaker = session_factory or AsyncSessionLocal
self._resolver = BackendResolver(settings, hikvision_runtime=hikvision_runtime)
self._registry = registry or SurgerySessionRegistry(settings=settings)
self._registry = registry or SurgerySessionRegistry()
self._archive = archive_persister or ArchivePersister(
settings=settings,
repository=result_repository,
session_factory=self._session_factory,
)
self._aggregator = WindowInferenceAggregator(settings=settings)
self._aggregator = WindowInferenceAggregator()
self._classifier_handler = VisionClassificationHandler(
settings=settings,
registry=self._registry,
)
@@ -157,7 +170,7 @@ class CameraSessionManager:
stop_event = asyncio.Event()
readies = [asyncio.Event() for _ in camera_ids]
tasks: list[asyncio.Task[None]] = []
open_timeout = self._s.video_open_timeout_sec + 5.0
open_timeout = bp.VIDEO_OPEN_TIMEOUT_SEC + 5.0
for cam_id, ready in zip(camera_ids, readies, strict=True):
tasks.append(
@@ -175,9 +188,18 @@ class CameraSessionManager:
run = RunningSurgery(stop_event=stop_event, state=state, tasks=tasks)
init_consumption_log_file(surgery_id)
init_voice_log_file(surgery_id, self._s)
init_voice_log_file(surgery_id)
await self._registry.register(surgery_id, run)
if ba.TEAR_SEGMENT_ENABLED:
primary = (ba.TEAR_SEGMENT_PRIMARY_CAMERA_ID or "").strip()
if primary and primary not in camera_ids:
logger.warning(
"撕段算法已开启但主摄 id={!r} 不在本台开录 camera_ids={} 中,该路不会跑撕段流水线",
primary,
camera_ids,
)
try:
await asyncio.wait_for(
asyncio.gather(*(r.wait() for r in readies)),
@@ -292,6 +314,45 @@ class CameraSessionManager:
# ------------------------------------------------------------------
# Camera worker拉流 + 推理节流 + 时间窗分桶 + 分类结果处理)
# ------------------------------------------------------------------
async def _finalize_tear_segment_runner(
self,
*,
surgery_id: str,
camera_id: str,
state: SurgerySessionState,
runner: TearGatedSegmentRunner,
) -> None:
recs = runner.finalize()
for rec in recs:
wall_ts = runner.wall_time_for_record(rec)
detail_ts = datetime.fromtimestamp(wall_ts, tz=timezone.utc)
await self._registry.append_confirmed_detail(
state=state,
item_id=rec.item_id,
item_name=rec.item_name,
doctor_id=bp.VIDEO_RESULT_DOCTOR_ID,
source="tear_segment",
cooldown_key=f"{surgery_id}:tear_seg:{rec.segment_index}",
detail_timestamp=detail_ts,
)
if ba.TEAR_SEGMENT_LOG_TXT and recs:
raw_tpl = (ba.TEAR_SEGMENT_LOG_TXT_PATH or "").strip()
if raw_tpl and "{surgery_id}" in raw_tpl:
p = Path(raw_tpl.format(surgery_id=surgery_id)).expanduser()
elif raw_tpl:
p = Path(raw_tpl).expanduser()
else:
p = Path(f"logs/tear_segment_{surgery_id}.txt")
labels_src = str(resolve_tear_segment_labels_yaml_path())
write_tear_segment_txt(
path=p,
surgery_id=surgery_id,
camera_id=camera_id,
labels_source=labels_src,
records=recs,
)
logger.info("撕段报告已写: {}", p)
async def _camera_worker(
self,
*,
@@ -311,74 +372,119 @@ class CameraSessionManager:
)
assert url is not None
last_infer = 0.0
async def _frame_handler(frame: object) -> None:
nonlocal last_infer
now = time.monotonic()
if now - last_infer < self._s.video_inference_interval_sec:
await asyncio.sleep(0.01)
return
last_infer = now
primary = (ba.TEAR_SEGMENT_PRIMARY_CAMERA_ID or "").strip()
use_tear_req = (
ba.TEAR_SEGMENT_ENABLED
and self._tear_models is not None
and primary
and camera_id == primary
)
runner: TearGatedSegmentRunner | None = None
if use_tear_req:
name_to_id = load_tear_segment_name_to_id()
try:
snap = await asyncio.to_thread(
self._vision.infer_frame_bgr,
frame,
state.name_to_code,
)
self._tear_models.ensure_loaded()
runner = self._tear_models.create_runner(name_to_id)
except Exception as exc:
logger.debug(
"Inference skip camera={} surgery={}: {}",
logger.exception(
"撕段模型未就绪,本路回退为原时间窗算法 camera={} surgery={}: {}",
camera_id,
surgery_id,
exc,
)
return
runner = None
if snap is None:
return
if runner is not None:
if self._s.video_log_inference_results:
logger.info(
"Vision result surgery={} camera={} top1={}({:.3f}) top2={}({:.3f}) top3={}({:.3f})",
surgery_id,
camera_id,
snap.t1_name,
snap.t1_conf,
snap.t2_name,
snap.t2_conf,
snap.t3_name,
snap.t3_conf,
async def _frame_handler_tear(frame: object) -> None:
await asyncio.to_thread(runner.process_frame_bgr, frame)
w_tear = CameraStreamWorker(
surgery_id=surgery_id,
camera_id=camera_id,
url=url,
)
try:
await w_tear.run(
stream_ready=stream_ready,
stop_event=stop_event,
frame_handler=_frame_handler_tear,
)
async with state.lock:
ready_windows = self._aggregator.ingest_snapshot_and_collect_ready(
finally:
await self._finalize_tear_segment_runner(
surgery_id=surgery_id,
camera_id=camera_id,
snap=snap,
state=state,
runner=runner,
)
else:
last_infer = 0.0
for win in ready_windows:
await self._classifier_handler.handle(
state=state,
cls_res=win.prediction,
ready=win,
surgery_id=surgery_id,
camera_id=camera_id,
)
async def _frame_handler(frame: object) -> None:
nonlocal last_infer
now = time.monotonic()
if now - last_infer < bp.VIDEO_INFERENCE_INTERVAL_SEC:
await asyncio.sleep(0.01)
return
last_infer = now
try:
snap = await asyncio.to_thread(
self._vision.infer_frame_bgr,
frame,
state.name_to_code,
)
except Exception as exc:
logger.debug(
"Inference skip camera={} surgery={}: {}",
camera_id,
surgery_id,
exc,
)
return
worker = CameraStreamWorker(
settings=self._s,
surgery_id=surgery_id,
camera_id=camera_id,
url=url,
)
await worker.run(
stream_ready=stream_ready,
stop_event=stop_event,
frame_handler=_frame_handler,
)
if snap is None:
return
if bp.VIDEO_LOG_INFERENCE_RESULTS:
logger.info(
"Vision result surgery={} camera={} top1={}({:.3f}) top2={}({:.3f}) top3={}({:.3f})",
surgery_id,
camera_id,
snap.t1_name,
snap.t1_conf,
snap.t2_name,
snap.t2_conf,
snap.t3_name,
snap.t3_conf,
)
async with state.lock:
ready_windows = self._aggregator.ingest_snapshot_and_collect_ready(
surgery_id=surgery_id,
camera_id=camera_id,
snap=snap,
state=state,
)
for win in ready_windows:
await self._classifier_handler.handle(
state=state,
cls_res=win.prediction,
ready=win,
surgery_id=surgery_id,
camera_id=camera_id,
)
worker = CameraStreamWorker(
surgery_id=surgery_id,
camera_id=camera_id,
url=url,
)
await worker.run(
stream_ready=stream_ready,
stop_event=stop_event,
frame_handler=_frame_handler,
)
finally:
if hik_user_id is not None and self._hik is not None:
await asyncio.to_thread(self._hik.logout, hik_user_id)