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

@@ -19,7 +19,7 @@ import yaml
from loguru import logger
from ultralytics import YOLO
from app.config import Settings, settings
from app.baked import algorithm as ba
def _ensure_yolo_config_dir() -> None:
@@ -361,14 +361,20 @@ def window_bucket_to_best_snap(
class ConsumableVisionAlgorithmService:
"""手部检测(可选)+ 耗材分类;供 CameraSessionManager 在视频线程中调用。"""
def __init__(self, app_settings: Settings | None = None) -> None:
def __init__(self, *, labels_yaml_path: str | None = None) -> None:
_ensure_yolo_config_dir()
self._s = app_settings or settings
self._labels_yaml_path = labels_yaml_path
self._det: YOLO | None = None
self._cls: YOLO | None = None
self._det_lock = Lock()
self._cls_lock = Lock()
def _labels_path(self) -> Path:
raw = self._labels_yaml_path
if raw is not None and str(raw).strip():
return Path(str(raw).strip()).expanduser()
return Path(ba.CONSUMABLE_CLASSIFIER_LABELS_YAML_PATH).expanduser()
def effective_candidate_consumables(self, requested: list[str]) -> list[str]:
"""请求体中的耗材子集;未提供(缺省或仅空白)时先用 ``consumable_classifier_labels.yaml`` 的 ``names``,无有效 YAML 则分类模型类名。"""
out: list[str] = []
@@ -382,7 +388,7 @@ class ConsumableVisionAlgorithmService:
if out:
return out
yaml_path = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
yaml_path = self._labels_path()
if yaml_path.is_file():
ylist = list_sorted_class_names_from_yaml(yaml_path)
if ylist:
@@ -404,7 +410,7 @@ class ConsumableVisionAlgorithmService:
if not candidates_norm:
return {}
yaml_path = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
yaml_path = self._labels_path()
yaml_map: dict[str, str] = {}
if yaml_path.is_file():
try:
@@ -420,19 +426,14 @@ class ConsumableVisionAlgorithmService:
return out
def _det_weights(self) -> Path | None:
raw = (self._s.hand_detection_weights or "").strip()
raw = (ba.HAND_DETECTION_WEIGHTS or "").strip()
if not raw:
return None
p = Path(raw).expanduser()
return p if p.is_file() else None
def _cls_weights(self) -> Path:
raw = (self._s.consumable_classifier_weights or "").strip()
if not raw:
raise ModelNotConfiguredError(
"未配置耗材分类权重。请设置 CONSUMABLE_CLASSIFIER_WEIGHTS。"
)
p = Path(raw).expanduser().resolve()
p = Path(ba.CONSUMABLE_CLASSIFIER_WEIGHTS).expanduser().resolve()
if not p.is_file():
raise ModelNotConfiguredError(f"耗材分类权重不存在: {p}")
return p
@@ -468,7 +469,7 @@ class ConsumableVisionAlgorithmService:
imgsz_det: int,
) -> np.ndarray | None:
h, w = frame.shape[:2]
device = resolve_inference_device(self._s.hand_detection_device)
device = resolve_inference_device(ba.HAND_DETECTION_DEVICE)
results = det_model.predict(
frame,
conf=det_conf,
@@ -499,28 +500,28 @@ class ConsumableVisionAlgorithmService:
crop = self.hand_crop(
frame,
det_model,
det_conf=self._s.hand_detection_conf,
pad_ratio=self._s.hand_detection_pad_ratio,
min_crop_px=self._s.hand_detection_min_crop_px,
imgsz_det=self._s.hand_detection_imgsz,
det_conf=ba.HAND_DETECTION_CONF,
pad_ratio=ba.HAND_DETECTION_PAD_RATIO,
min_crop_px=ba.HAND_DETECTION_MIN_CROP_PX,
imgsz_det=ba.HAND_DETECTION_IMGSZ,
)
if crop is None:
return None
else:
crop = frame
device = resolve_inference_device(self._s.consumable_classifier_device)
device = resolve_inference_device(ba.CONSUMABLE_CLASSIFIER_DEVICE)
try:
r = cls_model.predict(
crop,
imgsz=self._s.consumable_classifier_imgsz,
imgsz=ba.CONSUMABLE_CLASSIFIER_IMGSZ,
device=device,
verbose=False,
)
except Exception as exc:
raise PredictionError(f"耗材分类推理失败: {exc}") from exc
yp = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
yp = self._labels_path()
if yp.is_file():
st = yp.stat()
index_to_label_id = _cached_index_to_label_id(
@@ -537,7 +538,7 @@ class ConsumableVisionAlgorithmService:
)
if snap is None:
return None
if snap.t1_conf < self._s.consumable_min_cls_confidence:
if snap.t1_conf < ba.CONSUMABLE_MIN_CLS_CONFIDENCE:
return None
pname = snap.t1_name
if not pname: