feat: 消费停录汇总与查结果同口径,并更新分类与 TSV

- 停录时由 details 经 build_consumption_summary 写 TSV/终端汇总,移除 consumption_log_totals 与时间窗累加
- 更新 consumable_classifier 权重、YAML 标签与 consumable_vision_algorithm
- 扩展 consumption TSV 相关测试与配置注释

Made-with: Cursor
This commit is contained in:
Kevin
2026-04-24 14:27:56 +08:00
parent 557fcee803
commit b651364877
9 changed files with 318 additions and 155 deletions

View File

@@ -5,8 +5,9 @@
from __future__ import annotations
from typing import Any
import functools
import os
from typing import Any
import sys
from collections import Counter
from dataclasses import dataclass
@@ -126,6 +127,38 @@ def load_name_to_label_id_from_yaml(path: Path) -> dict[str, str]:
return out
def load_index_to_label_id_from_yaml(path: Path) -> dict[int, str]:
"""与 ``label_id`` 段:类索引 -> 业务 id 字符串;类名与 YAML 略有不一致时仍可落盘到正确 id。"""
try:
raw = path.read_text(encoding="utf-8")
except OSError:
return {}
try:
data: Any = yaml.safe_load(raw)
except yaml.YAMLError:
return {}
if not isinstance(data, dict):
return {}
label_raw = data.get("label_id")
if not isinstance(label_raw, dict):
return {}
out: dict[int, str] = {}
for k, v in label_raw.items():
try:
i = int(k)
except (TypeError, ValueError):
continue
if v is None or (isinstance(v, str) and not str(v).strip()):
continue
out[i] = str(v).strip()
return out
@functools.lru_cache(maxsize=8)
def _cached_index_to_label_id(path_resolved: str, mtime_ns: int) -> dict[int, str]:
return load_index_to_label_id_from_yaml(Path(path_resolved))
def list_sorted_class_names_from_yaml(path: Path) -> list[str]:
"""自 ``names`` 段按类索引升序取类名字符串(与训练/权重一致)。"""
try:
@@ -218,7 +251,11 @@ def _probs_data_to_numpy1d(raw) -> np.ndarray:
def cls_top3_from_result(
cls: YOLO, r, name_to_code: dict[str, str]
cls: YOLO,
r,
name_to_code: dict[str, str],
*,
index_to_label_id: dict[int, str] | None = None,
) -> ClsTop3 | None:
pr = r[0].probs
if pr is None:
@@ -244,6 +281,7 @@ def cls_top3_from_result(
n2 = n3 = ""
c2 = c3 = 0.0
i2 = i3 = -1
if len(t5i) > 1:
i2 = int(t5i[1])
n2 = str(cls.names.get(i2, "")).strip()
@@ -253,12 +291,19 @@ def cls_top3_from_result(
n3 = str(cls.names.get(i3, "")).strip()
c3 = _conf_for_idx(i3)
def _pid(label: str) -> str:
idx_extras = index_to_label_id or {}
def _pid(label: str, class_idx: int) -> str:
lb = (label or "").strip()
if not lb:
return ""
norm = _norm_product_name(lb)
return (name_to_code.get(norm) or name_to_code.get(lb) or "").strip()
c = (name_to_code.get(norm) or name_to_code.get(lb) or "").strip()
if c:
return c
if class_idx >= 0 and class_idx in idx_extras:
return idx_extras[class_idx]
return ""
return ClsTop3(
t1_name=n1,
@@ -267,9 +312,9 @@ def cls_top3_from_result(
t2_conf=c2,
t3_name=n3,
t3_conf=c3,
t1_pid=_pid(n1),
t2_pid=_pid(n2),
t3_pid=_pid(n3),
t1_pid=_pid(n1, t1i),
t2_pid=_pid(n2, i2),
t3_pid=_pid(n3, i3),
)
@@ -475,12 +520,31 @@ class ConsumableVisionAlgorithmService:
except Exception as exc:
raise PredictionError(f"耗材分类推理失败: {exc}") from exc
snap = cls_top3_from_result(cls_model, r, name_to_code)
yp = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
if yp.is_file():
st = yp.stat()
index_to_label_id = _cached_index_to_label_id(
str(yp.resolve()), st.st_mtime_ns
)
else:
index_to_label_id = {}
snap = cls_top3_from_result(
cls_model,
r,
name_to_code,
index_to_label_id=index_to_label_id,
)
if snap is None:
return None
if snap.t1_conf < self._s.consumable_min_cls_confidence:
return None
pname = snap.t1_name
if not pname or pname not in whitelist:
if not pname:
return None
return snap
pnorm = _norm_product_name(pname)
if pnorm in whitelist or pname in whitelist:
return snap
if (snap.t1_pid or "").strip():
return snap
return None