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