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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user