diff --git a/.env.example b/.env.example
index c1cdfb5..060bca0 100644
--- a/.env.example
+++ b/.env.example
@@ -31,9 +31,8 @@ CONSUMABLE_CLASSIFIER_TOPK=5
# CONSUMABLE_MIN_CLS_CONFIDENCE=0.5
# 时间窗(秒):窗内多次推理取众数后再走自动记账 / 待确认。
# CONSUMABLE_VISION_WINDOW_SEC=15
-# 可选:Excel「商品名称」「产品编码」表;空则物品 id 用名称。
-# 开始手术请求体 candidate_consumables 缺省或 [] 时,优先用本表全部商品名参与推理;未配置则用分类模型全部类名。
-# CONSUMABLE_CATALOG_XLSX_PATH=/path/to/视频中的商品信息表.xlsx
+# 业务物品 id 与开录时「空 candidate」的类名全表:app/resources/consumable_classifier_labels.yaml(names + label_id;同名多规格为 id1/id2/...)。未设或空则默认该包内文件。
+# CONSUMABLE_CLASSIFIER_LABELS_YAML_PATH=/app/app/resources/consumable_classifier_labels.yaml
# HAND_DETECTION_WEIGHTS=/absolute/path/to/hand_detect.pt
# HAND_DETECTION_IMGSZ=640
# HAND_DETECTION_CONF=0.25
diff --git a/app/config.py b/app/config.py
index 1e47942..717f8c6 100644
--- a/app/config.py
+++ b/app/config.py
@@ -58,7 +58,7 @@ class _VideoGroup(_SettingsGroup):
"consumable_classifier_device",
"consumable_classifier_topk",
"consumable_min_cls_confidence",
- "consumable_catalog_xlsx_path",
+ "consumable_classifier_labels_yaml_path",
"consumable_vision_window_sec",
"hand_detection_weights",
"hand_detection_imgsz",
@@ -172,6 +172,11 @@ def _default_camera_rtsp_urls_sample_path() -> str:
return str(_PACKAGE_DIR / "resources" / "camera_rtsp_urls.sample.json")
+def _default_consumable_classifier_labels_yaml() -> str:
+ """与分类训练类名、业务 `label_id` 对照的 YAML;见 `app/resources/consumable_classifier_labels.yaml`。"""
+ return str(_PACKAGE_DIR / "resources" / "consumable_classifier_labels.yaml")
+
+
class Settings(BaseSettings):
"""Application configuration loaded from environment / .env."""
@@ -199,8 +204,8 @@ class Settings(BaseSettings):
consumable_classifier_topk: int = 5
#: 耗材分类 top1 最低置信度(手部 ROI 或全帧送入分类器后的门槛)。
consumable_min_cls_confidence: float = Field(default=0.5, ge=0.0, le=1.0)
- #: 可选:`视频中的商品信息表.xlsx`(含「商品名称」「产品编码」);空则物品 id 用名称本身。
- consumable_catalog_xlsx_path: str = ""
+ #: 分类类名 + 业务 `label_id` 对照(与训练 `names` 一致);`build_name_mapping` 将识别出的类名匹配至此得到业务 id。空则使用下方默认包内文件。
+ consumable_classifier_labels_yaml_path: str = ""
#: 与离线脚本一致的时间窗(秒);窗内多次推理取众数后再走自动记账 / 语音追问逻辑。
consumable_vision_window_sec: float = Field(default=15.0, ge=0.5, le=600.0)
#: 手部检测 YOLO 权重;空或文件不存在时退化为「全帧送分类器」(兼容仅有关分类权重的环境)。
@@ -354,6 +359,13 @@ class Settings(BaseSettings):
return _default_consumable_classifier_weights()
return str(value)
+ @field_validator("consumable_classifier_labels_yaml_path", mode="before")
+ @classmethod
+ def consumable_classifier_labels_yaml_path_default(cls, value: object) -> str:
+ if value is None or str(value).strip() == "":
+ return _default_consumable_classifier_labels_yaml()
+ return str(value).strip()
+
model_config = SettingsConfigDict(
env_file=(str(_DEFAULT_ENV_FILE),),
env_file_encoding="utf-8",
diff --git a/app/resources/consumable_classifier_labels.yaml b/app/resources/consumable_classifier_labels.yaml
index 3b92b3f..5ddf359 100644
--- a/app/resources/consumable_classifier_labels.yaml
+++ b/app/resources/consumable_classifier_labels.yaml
@@ -1,48 +1,93 @@
-# 耗材分类器 consumable_classifier.pt 的 YOLO-cls 类别名(与训练侧 data.yaml 对齐)。
-# 推理时标签以权重内嵌 names 为准;本文件仅供文档与类别清单对照。
+# 与训练 data.yaml 类名一一对应;多规格时 label_id 以 / 连接;业务唯一来源。
+# 耗材分类器 consumable_classifier.pt 的 YOLO-cls 类别名应与训练 data.yaml 一致;
+# 业务 label_id 与 names 下标一一对应;推理时以权重内嵌 names 为准。
names:
- 0: MCuⅡ功能性宫内节育器
- 1: 一次性中性电极板
- 2: 一次性使用乳胶导尿管
- 3: 一次性使用冲洗袋
- 4: 一次性使用医疗卫生用品
- 5: 一次性使用单极手术电极
- 6: 一次性使用导尿管
- 7: 一次性使用手术单
- 8: 一次性使用手术单(一次性医用垫单)
- 9: 一次性使用手术衣
- 10: 一次性使用无菌敷贴
- 11: 一次性使用无菌气管插管Tracheal Tube
- 12: 一次性使用无菌注射器带针
- 13: 一次性使用无菌采样拭子
- 14: 一次性使用气管插管
- 15: 一次性使用灭菌棉签
- 16: 一次性使用灭菌橡胶外科手套
- 17: 一次性使用牙垫
- 18: 一次性使用精密过滤输液器 带针
- 19: 一次性使用肛门管
- 20: 一次性使用胃管
- 21: 一次性使用血液透析管路
- 22: 一次性使用输卵管导管
- 23: 一次性使用雾化器
- 24: 一次性使用静脉留置针
- 25: 一次性使用静脉输液针
- 26: 一次性使用麻醉面罩
- 27: 一次性内窥镜护套
- 28: 一次性医用灭菌棉签
- 29: 一次性无菌喉罩
- 30: 医用凡士林敷料
- 31: 医用纱布敷料
- 32: 医用缝合针
- 33: 医用脱脂棉纱布块
- 34: 可吸收性外科缝线
- 35: 密闭式防针刺伤型静脉留置针
- 36: 导管固定器
- 37: 气管切开插管
- 38: 结扎夹Ligating Clips
- 39: 自粘性薄膜敷料
- 40: 血液净化装置的体外循环血路
- 41: 负压引流器
- 42: 非吸收性外科缝线
- 43: 非吸收性外科缝线(蚕丝线)
-nc: 44
+ 0: 医用纱布敷料
+ 1: 一次性使用无菌注射器带针
+ 2: 一次性使用手术单
+ 3: 一次性使用肛门管
+ 4: 医用缝合针
+ 5: 非吸收性外科缝线(蚕丝线)
+ 6: 一次性中性电极板
+ 7: 一次性使用胃管
+ 8: 一次性使用输卵管导管
+ 9: 医用脱脂棉纱布块
+ 10: 导管固定器
+ 11: 可吸收性外科缝线
+ 12: 一次性使用医疗卫生用品
+ 13: 一次性医用灭菌棉签
+ 14: 一次性使用静脉输液针
+ 15: 非吸收性外科缝线
+ 16: 一次性使用冲洗袋
+ 17: 医用凡士林敷料
+ 18: 一次性使用无菌敷贴
+ 19: 自粘性薄膜敷料
+ 20: 密闭式防针刺伤型静脉留置针
+ 21: 一次性使用静脉留置针
+ 22: 一次性使用手术单(一次性医用垫单)
+ 23: 一次性使用无菌采样拭子
+ 24: 一次性使用手术衣
+ 25: 一次性使用气管插管
+ 26: 一次性使用牙垫
+ 27: 一次性使用血液透析管路
+ 28: 血液净化装置的体外循环血路
+ 29: 一次性使用精密过滤输液器 带针
+ 30: 结扎夹Ligating Clips
+ 31: 一次性使用麻醉面罩
+ 32: 一次性内窥镜护套
+ 33: 一次性使用导尿管
+ 34: 一次性使用灭菌橡胶外科手套
+ 35: 一次性无菌喉罩
+ 36: 一次性使用无菌气管插管Tracheal Tube
+ 37: 负压引流器
+ 38: 一次性使用单极手术电极
+ 39: 一次性使用乳胶导尿管
+ 40: 气管切开插管
+ 41: 一次性使用雾化器
+ 42: MCuⅡ功能性宫内节育器
+# 与 names 的整数下标一一对应;多规格同名则为 id1/id2/... 。
+label_id:
+ 0: 19246-3-14
+ 1: 1531-3-2/1531-3-1/1174-42-4/1174-42-1
+ 2: 14764-2-4
+ 3: 1518-22-4
+ 4: 583039/11207-1-64
+ 5: 654032
+ 6: 4787-2-55
+ 7: 1518-34-17
+ 8: 1380-15-1
+ 9: 8028-4-39
+ 10: 1441340
+ 11: 11765-1-101/1330-49-185
+ 12: 2241272
+ 13: 2237844/10183-1-29
+ 14: 129-5-30
+ 15: 4142-1-46
+ 16: 1644-37-3
+ 17: 10870-25-16
+ 18: 215-93-1
+ 19: 1819-4-1
+ 20: 1281-39-3
+ 21: 12591-1-184
+ 22: 21504-1-1
+ 23: 15026-1-1
+ 24: 8386-24-1
+ 25: 21444-1-2
+ 26: 975961
+ 27: 14730-10-10
+ 28: 739-2-1
+ 29: 2295950
+ 30: 14780-2-12
+ 31: 2003984
+ 32: 521-31-1
+ 33: 735592/14556-4-18
+ 34: 10362-1-4
+ 35: 7386-61-46
+ 36: 14780-3-5
+ 37: 1518-20-8
+ 38: 4805-2-50
+ 39: 7386-10-89
+ 40: 10869-30-7
+ 41: 5019-4-43
+ 42: 740-2-14
+nc: 43
diff --git a/app/schemas.py b/app/schemas.py
index cd79a49..dd181b5 100644
--- a/app/schemas.py
+++ b/app/schemas.py
@@ -36,8 +36,8 @@ class SurgeryStartRequest(BaseModel):
description=(
"本次手术可能使用到的耗材子集(可选)。"
"非空时仅对该清单内名称做自动记账与待确认追问。"
- "缺省或空数组时,使用服务端配置的耗材目录 Excel 全部商品名;"
- "未配置目录则使用分类模型全部类名。"
+ "缺省或空数组时,使用 consumable_classifier_labels.yaml 中全部类名;"
+ "无有效 yaml 则使用分类模型全部类名。"
),
)
@@ -96,8 +96,8 @@ class SurgeryConsumptionDetail(BaseModel):
item_id: str = Field(
description=(
- "业务物品标识:优先为耗材目录中的产品编码;"
- "目录键经名称归一化后与分类类名匹配,未命中目录时与模型输出类名一致。"
+ "业务物品标识:来自 consumable_classifier_labels.yaml 的 label_id;"
+ "类名经归一化后匹配,未在 yaml 中配置 label_id 时与模型类名相同。"
),
)
item_name: str = Field(description="物品名称(分类或确认后的展示名)。")
diff --git a/app/services/consumable_vision_algorithm.py b/app/services/consumable_vision_algorithm.py
index 9a3f827..af77bb6 100644
--- a/app/services/consumable_vision_algorithm.py
+++ b/app/services/consumable_vision_algorithm.py
@@ -5,7 +5,7 @@
from __future__ import annotations
-import math
+from typing import Any
import os
import sys
from collections import Counter
@@ -14,8 +14,8 @@ from pathlib import Path
from threading import Lock
import numpy as np
+import yaml
from loguru import logger
-from openpyxl import load_workbook
from ultralytics import YOLO
from app.config import Settings, settings
@@ -79,22 +79,6 @@ class ClsTop3:
t3_pid: str
-def _find_col_idx(headers: list[object], want: str) -> int | None:
- want = want.strip()
- for i, h in enumerate(headers):
- if str(h).strip() == want:
- return i
- return None
-
-
-def _cell_empty(value: object) -> bool:
- if value is None:
- return True
- if isinstance(value, float) and math.isnan(value):
- return True
- return False
-
-
def _norm_product_name(name: str) -> str:
s = (name or "").strip()
if s == "一次性医用垫单":
@@ -102,49 +86,73 @@ def _norm_product_name(name: str) -> str:
return s
-def load_name_to_product_code(xlsx: Path) -> dict[str, str]:
- """商品名称 -> 产品编码(白名单键为归一化后的名称)。"""
- wb = load_workbook(filename=str(xlsx), read_only=True, data_only=True)
+def load_name_to_label_id_from_yaml(path: Path) -> dict[str, str]:
+ """从 ``consumable_classifier_labels.yaml`` 得到:归一化商品名 -> 业务 label_id(可与 ``names`` 下标一一对应;多规格为 ``a/b/...``)。"""
try:
- ws = wb.worksheets[0]
- rows = ws.iter_rows(values_only=True)
- header = next(rows, None)
- if header is None:
- raise ValueError("Excel 为空")
- headers = list(header)
- i_code = _find_col_idx(headers, "产品编码")
- i_name = _find_col_idx(headers, "商品名称")
- if i_code is None or i_name is None:
- raise ValueError("Excel 缺少「产品编码」或「商品名称」列")
+ raw = path.read_text(encoding="utf-8")
+ except OSError as exc:
+ logger.warning("无法读取耗材 label YAML {}: {}", path, exc)
+ return {}
+ try:
+ data: Any
+ data = yaml.safe_load(raw)
+ except yaml.YAMLError as exc:
+ logger.warning("解析耗材 label YAML 失败 {}: {}", path, exc)
+ return {}
+ if not isinstance(data, dict):
+ return {}
+ names_raw = data.get("names")
+ label_raw = data.get("label_id")
+ if not isinstance(names_raw, dict) or not isinstance(label_raw, dict):
+ return {}
+ out: dict[str, str] = {}
+ for k, v in names_raw.items():
+ try:
+ i = int(k)
+ except (TypeError, ValueError):
+ continue
+ name = str(v).strip() if v is not None else ""
+ if not name:
+ continue
+ lid: Any = None
+ if i in label_raw:
+ lid = label_raw[i]
+ elif str(i) in label_raw:
+ lid = label_raw[str(i)]
+ if lid is None or (isinstance(lid, str) and not str(lid).strip()):
+ continue
+ id_str = str(lid).strip()
+ out[_norm_product_name(name)] = id_str
+ return out
- m: dict[str, str] = {}
- dups: set[str] = set()
- for row in rows:
- if not row:
- continue
- raw = row[i_name] if i_name < len(row) else None
- if _cell_empty(raw):
- continue
- n = _norm_product_name(str(raw).strip())
- if not n:
- continue
- code = row[i_code] if i_code < len(row) else None
- if _cell_empty(code):
- continue
- sc = str(code).strip()
- if n in m and m[n] != sc:
- dups.add(n)
- continue
- if n not in m:
- m[n] = sc
- finally:
- wb.close()
- if dups:
- logger.warning(
- "Excel 中以下商品名称对应多组产品编码,已保留首次映射: {}",
- ";".join(sorted(dups)[:12]) + (" …" if len(dups) > 12 else ""),
- )
- return m
+
+def list_sorted_class_names_from_yaml(path: Path) -> list[str]:
+ """自 ``names`` 段按类索引升序取类名字符串(与训练/权重一致)。"""
+ 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 []
+ names_raw = data.get("names")
+ if not isinstance(names_raw, dict):
+ return []
+ items: list[tuple[int, str]] = []
+ for k, v in names_raw.items():
+ try:
+ i = int(k)
+ except (TypeError, ValueError):
+ continue
+ s = str(v).strip() if v is not None else ""
+ if not s:
+ continue
+ items.append((i, _norm_product_name(s)))
+ items.sort(key=lambda t: t[0])
+ return [n for _, n in items]
def collect_hand_boxes(model: YOLO, boxes) -> list[tuple[float, float, float, float]]:
@@ -317,7 +325,7 @@ class ConsumableVisionAlgorithmService:
self._cls_lock = Lock()
def effective_candidate_consumables(self, requested: list[str]) -> list[str]:
- """请求体中的耗材子集;未提供(缺省或仅空白)时用目录 Excel 全部商品名,无目录则用分类模型全部类名。"""
+ """请求体中的耗材子集;未提供(缺省或仅空白)时先用 ``consumable_classifier_labels.yaml`` 的 ``names``,无有效 YAML 则分类模型类名。"""
out: list[str] = []
seen: set[str] = set()
for c in requested:
@@ -329,23 +337,12 @@ class ConsumableVisionAlgorithmService:
if out:
return out
- xlsx_raw = (self._s.consumable_catalog_xlsx_path or "").strip()
- if xlsx_raw:
- path = Path(xlsx_raw).expanduser()
- if path.is_file():
- try:
- full = load_name_to_product_code(path)
- except Exception as exc:
- logger.warning("读取耗材目录 Excel 失败,回退到模型类名: {}", exc)
- else:
- if full:
- return sorted(full.keys())
- logger.warning("耗材目录 Excel 无有效行,回退到模型类名")
- else:
- logger.warning(
- "耗材目录 Excel 路径已配置但文件不存在: {},回退到模型类名",
- path,
- )
+ yaml_path = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
+ if yaml_path.is_file():
+ ylist = list_sorted_class_names_from_yaml(yaml_path)
+ if ylist:
+ return ylist
+ logger.warning("耗材 label YAML 中无有效 names: {}", yaml_path)
cls_model = self._get_cls()
labels = sorted(
@@ -356,21 +353,26 @@ class ConsumableVisionAlgorithmService:
def build_name_mapping(
self, candidate_consumables: list[str]
) -> dict[str, str]:
- """分类标签 -> 业务物品 id(Excel 产品编码;无表时用名称自身)。"""
+ """分类类名(归一化) -> 业务 id:仅 ``consumable_classifier_labels.yaml`` 的 ``label_id``;无映射时用语义类名作 id。"""
stripped = [_norm_product_name(c.strip()) for c in candidate_consumables if c.strip()]
candidates_norm = {n: n for n in stripped}
- xlsx_raw = (self._s.consumable_catalog_xlsx_path or "").strip()
- if xlsx_raw:
- path = Path(xlsx_raw).expanduser()
- if path.is_file():
- full = load_name_to_product_code(path)
- out: dict[str, str] = {}
- for norm in candidates_norm:
- if norm in full:
- out[norm] = full[norm]
- return out
- logger.warning("耗材目录 Excel 路径已配置但文件不存在: {}", path)
- return {n: n for n in candidates_norm}
+ if not candidates_norm:
+ return {}
+
+ yaml_path = Path(self._s.consumable_classifier_labels_yaml_path).expanduser()
+ yaml_map: dict[str, str] = {}
+ if yaml_path.is_file():
+ try:
+ yaml_map = load_name_to_label_id_from_yaml(yaml_path)
+ except Exception as exc: # noqa: BLE001
+ logger.warning("加载耗材 label YAML 失败 {}: {}", yaml_path, exc)
+ else:
+ logger.debug("耗材 label YAML 不存在: {}", yaml_path)
+
+ out: dict[str, str] = {}
+ for norm in candidates_norm:
+ out[norm] = yaml_map.get(norm) or norm
+ return out
def _det_weights(self) -> Path | None:
raw = (self._s.hand_detection_weights or "").strip()
diff --git a/app/services/consumption_tsv_log.py b/app/services/consumption_tsv_log.py
index 327f511..2905a80 100644
--- a/app/services/consumption_tsv_log.py
+++ b/app/services/consumption_tsv_log.py
@@ -20,7 +20,7 @@ from app.services.consumable_vision_algorithm import ClsTop3, _norm_product_name
from app.terminal_markdown import print_markdown_stderr
# 制表符分隔;时间范围用 U+2013 连接;本窗消耗数量恒为 1。
-# top2/top3 为模型原始排序(未按手术候选重排);item_id 仅写产品编码,无编码时留空。
+# top2/top3 为模型原始排序(未按手术候选重排);item_id 只写与展示名不同的业务 id(label_id),与名称相同时留空。
HEADER = (
"item_id\titem_name\tqty\tdoctor_id\ttimestamp\t"
"top2_name\ttop2_conf\ttop3_name\ttop3_conf\n"
@@ -97,7 +97,7 @@ def resolve_consumption_ids(
) -> tuple[str, str]:
"""TSV 第一列 item_id 与内存汇总键。
- - ``tsv_item_id``:仅产品编码(或模型侧 t1_pid);与展示名相同则视为无独立编码,留空。
+ - ``tsv_item_id``:业务 id(或模型侧 t1_pid);与展示名相同则视为无独立 id,留空。
- ``totals_key``:汇总用稳定键;无编码时用归一化名称,避免多行空 id 碰撞。
"""
n = (t1_name or "").strip()
diff --git a/app/services/video/session_manager.py b/app/services/video/session_manager.py
index 573dd69..be2ca67 100644
--- a/app/services/video/session_manager.py
+++ b/app/services/video/session_manager.py
@@ -140,7 +140,7 @@ class CameraSessionManager:
if not resolved:
raise SurgeryPipelineError(
"RECORDING_CANNOT_START",
- "耗材候选为空:请在请求中传入 candidate_consumables,或配置耗材目录 Excel / 分类模型。",
+ "耗材候选为空:请在请求中传入 candidate_consumables,或提供有效的 consumable_classifier_labels.yaml / 分类模型。",
)
if not any(str(x).strip() for x in candidate_consumables):
logger.info(
diff --git a/app/services/video/session_registry.py b/app/services/video/session_registry.py
index b5adfa8..c68e4da 100644
--- a/app/services/video/session_registry.py
+++ b/app/services/video/session_registry.py
@@ -59,7 +59,7 @@ class CameraStreamInferState:
@dataclass
class SurgerySessionState:
candidate_consumables: list[str]
- #: 分类类名(归一化) -> 业务物品 id(Excel 产品编码或名称)。
+ #: 分类类名(归一化) -> 业务物品 id(YAML label_id 或类名)
name_to_code: dict[str, str] = field(default_factory=dict)
camera_infer: dict[str, CameraStreamInferState] = field(default_factory=dict)
details: list[SurgeryConsumptionStored] = field(default_factory=list)
diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml
index 15ce272..fa9b821 100644
--- a/docker-compose.prod.yml
+++ b/docker-compose.prod.yml
@@ -38,7 +38,6 @@ services:
CONSUMABLE_CLASSIFIER_TOPK: ${CONSUMABLE_CLASSIFIER_TOPK:-5}
CONSUMABLE_MIN_CLS_CONFIDENCE: ${CONSUMABLE_MIN_CLS_CONFIDENCE:-0.5}
CONSUMABLE_VISION_WINDOW_SEC: ${CONSUMABLE_VISION_WINDOW_SEC:-15}
- CONSUMABLE_CATALOG_XLSX_PATH: ${CONSUMABLE_CATALOG_XLSX_PATH:-}
HAND_DETECTION_WEIGHTS: ${HAND_DETECTION_WEIGHTS:-}
HAND_DETECTION_IMGSZ: ${HAND_DETECTION_IMGSZ:-640}
HAND_DETECTION_DEVICE: ${HAND_DETECTION_DEVICE:-}
diff --git a/docs/staging-regression-checklist.md b/docs/staging-regression-checklist.md
index 97d24be..611a9f4 100644
--- a/docs/staging-regression-checklist.md
+++ b/docs/staging-regression-checklist.md
@@ -12,7 +12,7 @@
## 主流程
1. **开始手术** `POST /client/surgeries/start`
- - 请求体含 6 位 `surgery_id`、`camera_ids`;`candidate_consumables` 可空(空则全量目录/模型类名)
+ - 请求体含 6 位 `surgery_id`、`camera_ids`;`candidate_consumables` 可空(空则 yaml 全量类名/模型类名)
- 返回 `200`,日志中各路 RTSP 首帧就绪
2. **进行中查询(可选)** `GET /client/surgeries/{id}/result`
- 至少一条明细时 `200`;无明细(开录后尚无、已归档但零消耗等)为 `503 RESULT_NOT_READY`
diff --git a/docs/video-backends.md b/docs/video-backends.md
index b7b5fcb..397e0a9 100644
--- a/docs/video-backends.md
+++ b/docs/video-backends.md
@@ -33,7 +33,7 @@ SDK **不作为构建期依赖**:将厂商提供的 Linux x86_64 动态库挂
## 推理与结果查询
- 开录后按 `VIDEO_INFERENCE_INTERVAL_SEC` 抽帧,依次调用耗材分类与撕扯动作模型。
-- **候选耗材清单**(`candidate_consumables`):非空时**仅**清单内名称参与自动记账与待确认;**缺省或 `[]`** 时,用耗材目录 Excel **全部商品名**作为候选;无目录则用分类模型**全部类名**。
+- **候选耗材清单**(`candidate_consumables`):非空时**仅**清单内名称参与自动记账与待确认;**缺省或 `[]`** 时,用 `consumable_classifier_labels.yaml` 的 **全部类名**作为候选;yaml 无有效 `names` 时用分类模型**全部类名**。
- 当分类 Top1 置信度 **≥** `VIDEO_AUTO_CONFIRM_CONFIDENCE`(**默认 0.9**)且标签在候选清单内时,自动写入一条 `source=vision` 的消耗明细;**低于**该线的识别需人工确认(在语音下沿之上且能展示候选项时入队)。
- 置信度在 `VIDEO_VOICE_CONFIRM_MIN_CONFIDENCE`, `VIDEO_AUTO_CONFIRM_CONFIDENCE` 等区间且存在可向医生展示的候选时,会生成**待确认**任务;客户端 `GET /client/surgeries/{surgery_id}/pending-confirmation`,确认后 `POST .../pending-confirmation/{id}/resolve` 等。
- `GET /client/surgeries/{surgery_id}/result` 仅在存在**至少一条**消耗明细时返回 200;无明细(已开录但尚未记账、已结束但零消耗、或尚无归档等)返回 503 `RESULT_NOT_READY`。
diff --git a/docs/客户端手术通信接口说明.md b/docs/客户端手术通信接口说明.md
index 7fd97f6..dfd8028 100644
--- a/docs/客户端手术通信接口说明.md
+++ b/docs/客户端手术通信接口说明.md
@@ -1,180 +1,47 @@
-
-
# 手术室监控服务:客户端手术通信接口说明
-面向对接本 FastAPI 服务的客户端(HIS、手麻、工作站等)。字段、状态码与返回模型以 `/docs` 和 `/openapi.json` 为准,本文用于摘要和流程说明。
-
-> [!summary] 常用响应模型
-> - `SurgeryApiResponse`
-> - `SurgeryResultResponse`
-> - `SurgeryPendingConfirmationResponse`
-> - 业务错误外形见 `SurgeryClientErrorResponse`
-> - 内部演示页:`scripts/demo_client/`(仅供演示,不作为对外契约)
-
## 能力概览
-- 探活:`GET /health`,用于检查进程和数据库状态,详见 5.1 节。
-- 开始手术:`POST /client/surgeries/start`,只有在开录确认成功后才返回 `200`。
-- 结束手术:`POST /client/surgeries/end`,只有在停录确认成功后才返回 `200`。
-- 查询结果:`GET /client/surgeries/{surgery_id}/result`,至少存在一条消耗明细时返回 `200`;否则返回 `503`,常见错误码为 `RESULT_NOT_READY`。
-- 待确认播报:`GET /client/surgeries/{surgery_id}/pending-confirmation`,拉取队首低置信度任务,返回话术文本和 MP3 Base64。
-- 待确认答复:`POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。
-
-> [!important] HTTP 约定
-> - `start` 和 `end` 使用 `POST` + `application/json`
-> - `result` 使用 `GET`
-> - `resolve` 使用 `POST` + `multipart/form-data`
-> - `surgery_id` 固定为 6 位数字,正则为 `^\d{6}$`
-> - `resolve` 路径中的 `confirmation_id` 必须与待确认接口返回值一致
-> - `camera_ids` 必须与第 2 节清单及运维配置完全一致
+| **能力** | **说明** |
+| --------- | -------------------------------------------------------------------------------------------------------------------------------- |
+| **探活** | `GET /health`,用于检查进程和数据库状态,详见 5.1 节。 |
+| **开始手术** | `POST /client/surgeries/start`,只有在开录确认成功后才返回 `200`。 |
+| **结束手术** | `POST /client/surgeries/end`,只有在停录确认成功后才返回 `200`。 |
+| **查询结果** | `GET /client/surgeries/{surgery_id}/result`,至少存在一条消耗明细时返回 `200`;否则返回 `503`,常见错误码为 `RESULT_NOT_READY`。 |
+| **待确认播报** | `GET /client/surgeries/{surgery_id}/pending-confirmation`,拉取队首低置信度任务,返回话术文本和 MP3 Base64。 |
+| **待确认答复** | `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`,上传医生答复的 WAV 录音,服务端完成 ASR 后入账或关闭。该录音与播报音频无关。 |
## 1. 服务与基础信息
-- 协议:`HTTP/HTTPS`
-- 端口:`38080`,生产环境以实际入口为准
-- 路由:无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health`
-- `start` / `end` 请求体:JSON
-- `resolve` 请求体:`multipart/form-data`,字段名为 `audio`
-- 在线文档:`/docs`、`/redoc`
+| **项目** | **说明** |
+| ----------------------- | ------------------------------------------- |
+| **协议** | `HTTP/HTTPS` |
+| **端口** | `38080`,生产环境以实际入口为准 |
+| **路由** | 无全局前缀;业务接口位于 `/client/...`,健康检查位于 `/health` |
+| **`start` / `end` 请求体** | JSON |
+| **`resolve` 请求体** | `multipart/form-data`,字段名为 `audio` |
+| **在线文档** | `/docs`、`/redoc` |
## 2. 摄像头 ID 与 RTSP
-RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境(例如 JSON 映射或环境变量)。业务程序不在客户端保存 RTSP,客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`。
+RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运维,运维再写入服务端环境。客户端只在 `POST /client/surgeries/start` 中传 `camera_ids`。
-配置格式示例见 `app/resources/camera_rtsp_urls.sample.json`。配置项细节见 `.env.example` 与 `docs/video-backends.md`。
-
-**摄像头映射示例**
-
-- `or-cam-01`
- - RTSP:`rtsp://...`(由现场或 NVR 文档整理后交给运维)
- - 备注:术间、机位
-- `or-cam-02`
- - RTSP:`...`
- - 备注:`...`
-
-> [!warning] 对接要求
-> - `camera_ids` 必须与运维配置中的 key 完全一致
-> - RTSP 不应硬编码在客户端业务程序中
-
-> [!tip] 联调建议
-> - 运维配置完成后,客户端使用上面清单中的 `camera_id` 调用 `start` 验证是否返回 `200`
-> - 若返回 `503` 且 `detail.code = RECORDING_CANNOT_START`,优先核对 ID 拼写以及监控服务器侧网络连通性
+| **camera_id** | **RTSP** | **备注** |
+| ------------- | -------------------------------- | ------ |
+| `or-cam-01` | `rtsp://...`(由现场或 NVR 文档整理后交给运维) | 术间、机位 |
+| `or-cam-02` | `...` | `...` |
## 3. HTTP 路由一览
-1. `GET /health`:探活
-2. `POST /client/surgeries/start`:开始手术
-3. `POST /client/surgeries/end`:结束手术
-4. `GET /client/surgeries/{surgery_id}/result`:查询手术结果
-5. `GET /client/surgeries/{surgery_id}/pending-confirmation`:拉取待确认耗材
-6. `POST /client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`:提交待确认结果(WAV)
+| **序号** | **方法** | **路径** | **说明** |
+| ------ | ------ | ------------------------------------------------------------------------------- | ------- |
+| 1 | `GET` | `/health` | 探活 |
+| 2 | `POST` | `/client/surgeries/start` | 开始手术 |
+| 3 | `POST` | `/client/surgeries/end` | 结束手术 |
+| 4 | `GET` | `/client/surgeries/{surgery_id}/result` | 查询手术结果 |
+| 5 | `GET` | `/client/surgeries/{surgery_id}/pending-confirmation` | 拉取待确认耗材 |
+| 6 | `POST` | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` | 提交医生答复 |
+| | | | |
## 4. 流程
@@ -182,34 +49,34 @@ RTSP 地址、账号、口令等由客户端对接工程师提供给服务端运
```mermaid
sequenceDiagram
- participant Client as 客户系统
- participant API as 监控服务 API
+ participant Client as 客户端
+ participant Server as 服务端
- Client->>API: POST /client/surgeries/start
- Note over Client,API: body: surgery_id, camera_ids, candidate_consumables
- API-->>Client: 200 accepted(开录已确认)
+ Client->>Server: POST /client/surgeries/start
+ Note over Client,Server: body: surgery_id, camera_ids, candidate_consumables
+ Server-->>Client: 200 accepted(开录已确认)
par 术中
loop 轮询结果
- Client->>API: GET .../result
- API-->>Client: 200 或 503 RESULT_NOT_READY
+ Client->>Server: GET .../result
+ Server-->>Client: 200 或 503 RESULT_NOT_READY
end
- loop 轮询待确认(若启用)
- Client->>API: GET .../pending-confirmation
- API-->>Client: 200 或 404
+ loop 轮询待确认
+ Client->>Server: GET .../pending-confirmation
+ Server-->>Client: 200 或 404
opt 有待确认
Client->>Client: 播放 prompt_audio_mp3_base64
- Client->>API: POST .../resolve(multipart audio)
- API-->>Client: 200 accepted
+ Client->>Server: POST .../resolve(multipart audio)
+ Server-->>Client: 200 accepted
end
end
end
- Client->>API: POST /client/surgeries/end
- API-->>Client: 200 accepted(停录已确认)
+ Client->>Server: POST /client/surgeries/end
+ Server-->>Client: 200 accepted(停录已确认)
- Client->>API: GET .../result
- API-->>Client: 200(持久化后可查时返回)
+ Client->>Server: GET .../result
+ Server-->>Client: 200(持久化后可查时返回)
```
### 4.2 状态图
@@ -226,21 +93,20 @@ flowchart LR
## 5. 接口详情
-以下按“基本信息 -> 请求 -> 响应 -> 状态码”组织,与 OpenAPI 中 `tags: client` 和 `health` 一致。
-### 5.0 通用约定
+**路径参数 `surgery_id`**
-**路径参数 `surgery_id`**
-
-- 长度:固定 `6`
-- 字符集:仅数字
-- 正则:`^\d{6}$`
+| **约束项** | **说明** |
+| ------- | --------- |
+| **长度** | 固定 `6` |
+| **字符集** | 仅数字 |
+| **正则** | `^\d{6}$` |
**业务错误响应**
-多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
+多数业务失败在 `4xx` 或 `5xx` 下返回如下 JSON:
-```json
+```
{
"detail": {
"code": "错误码字符串",
@@ -254,69 +120,63 @@ flowchart LR
**基本信息**
-- 方法:`GET`
-- 路径:`/health`
-- 请求体:无
+| **项目** | **内容** |
+| ------- | --------- |
+| **方法** | `GET` |
+| **路径** | `/health` |
+| **请求体** | 无 |
**响应说明**
-- `200`
- - 说明:进程正常且数据库可连通
- - 响应体示例:`{"status":"ok","database":"connected"}`
-- `503`
- - 说明:数据库不可用(降级)
- - 响应体示例:`{"status":"degraded","database":"unavailable"}`
+| **HTTP** | **说明** | **响应体示例** |
+| -------- | ----------- | ------------------------------------------------ |
+| `200` | 进程正常且数据库可连通 | `{"status":"ok","database":"connected"}` |
+| `503` | 数据库不可用(降级) | `{"status":"degraded","database":"unavailable"}` |
### 5.2 开始手术
**基本信息**
-- 方法:`POST`
-- 路径:`/client/surgeries/start`
-- Content-Type:`application/json; charset=utf-8`
+| **项目** | **内容** |
+| ---------------- | --------------------------------- |
+| **方法** | `POST` |
+| **路径** | `/client/surgeries/start` |
+| **Content-Type** | `application/json; charset=utf-8` |
**业务说明**
-- 服务端会为 `camera_ids` 中的每个摄像头建立拉流与推理任务,只有在确认开录成功(如首帧就绪)后才返回 HTTP `200`
-- 若同一 `surgery_id` 存在尚未落库的历史归档,服务端会先尝试写入数据库;失败时可能返回 `503`(如 `RECORDING_CANNOT_START`),以避免静默丢数据
-- `candidate_consumables` 为空时,服务端会展开为目录 Excel 中的全部商品名,或在未配置目录时展开为分类模型的全部类名
+- 服务端会为 `camera_ids` 中的每个摄像头建立拉流与推理任务,只有在确认开录成功(如首帧就绪)后才返回 HTTP `200`。
+
+- `candidate_consumables` 为空时,服务端会展开为目录中的全部耗材名。
+
**请求体(JSON)**
-- `surgery_id`
- - 类型:`string`
- - 必填:是
- - 说明:6 位数字,与路径规则一致
-- `camera_ids`
- - 类型:`string[]`
- - 必填:是
- - 说明:至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节
-- `candidate_consumables`
- - 类型:`string[]`
- - 必填:否
- - 说明:非空时仅这些名称参与自动记账与待确认;缺省或 `[]` 时使用全部候选
+| **字段** | **类型** | **必填** | **说明** |
+| ----------------------- | ---------- | ------ | ----------------------------------- |
+| `surgery_id` | `string` | 是 | 6 位数字 |
+| `camera_ids` | `string[]` | 是 | 至少 1 个;必须与运维配置的摄像头 ID 完全一致,见第 2 节 |
+| `candidate_consumables` | `string[]` | 否 | 非空时仅这些名称参与自动记账与待确认;缺省或 `[]` 时使用全部候选 |
**响应体(200)**
-- `surgery_id`
- - 类型:`string`
- - 说明:与请求一致
-- `status`
- - 类型:`string`
- - 说明:成功时通常为 `accepted`
-- `message`
- - 类型:`string`
- - 说明:说明文案
+| **字段** | **类型** | **说明** |
+| ------------ | -------- | ----------------- |
+| `surgery_id` | `string` | 与请求一致 |
+| `status` | `string` | 成功时通常为 `accepted` |
+| `message` | `string` | 说明文案 |
**状态码**
-- `200`:开录已确认
-- `422`:参数校验失败,例如 `surgery_id` 非 6 位或 `camera_ids` 为空数组
-- `503`:开录未确认或录制子系统故障;`detail.code` 常见为 `RECORDING_CANNOT_START`
+| **HTTP** | **说明** |
+| -------- | -------------------------------------------------------- |
+| `200` | 开录已确认 |
+| `422` | 参数校验失败,例如 `surgery_id` 非 6 位或 `camera_ids` 为空数组 |
+| `503` | 开录未确认或录制子系统故障;`detail.code` 常见为 `RECORDING_CANNOT_START` |
**请求示例**
-```json
+```
{
"surgery_id": "123456",
"camera_ids": ["or-cam-01", "or-cam-02"],
@@ -326,7 +186,7 @@ flowchart LR
**响应示例(200)**
-```json
+```
{
"surgery_id": "123456",
"status": "accepted",
@@ -338,34 +198,37 @@ flowchart LR
**基本信息**
-- 方法:`POST`
-- 路径:`/client/surgeries/end`
-- Content-Type:`application/json; charset=utf-8`
+| **项目** | **内容** |
+| ---------------- | --------------------------------- |
+| **方法** | `POST` |
+| **路径** | `/client/surgeries/end` |
+| **Content-Type** | `application/json; charset=utf-8` |
**业务说明**
-停止该 `surgery_id` 关联的全部摄像头任务,只有在确认停录完成后才返回 `200`。
+停止该 `surgery_id` 关联的全部摄像头任务,只有在确认停录完成后才返回 `200`。
**请求体(JSON)**
-- `surgery_id`
- - 类型:`string`
- - 必填:是
- - 说明:6 位数字
+| **字段** | **类型** | **必填** | **说明** |
+| ------------ | -------- | ------ | ------ |
+| `surgery_id` | `string` | 是 | 6 位数字 |
**响应体(200)**
-字段含义与 5.2 节一致,`message` 示例为 `摄像头录制已停止,手术已结束。`
+字段含义与 5.2 节一致,`message` 示例为 `摄像头录制已停止,手术已结束。`
**状态码**
-- `200`:停录已确认
-- `422`:参数校验失败
-- `503`:停录未确认或故障;`detail.code` 常见为 `RECORDING_NOT_STOPPED`
+| **HTTP** | **说明** |
+| -------- | -------------------------------------------------- |
+| `200` | 停录已确认 |
+| `422` | 参数校验失败 |
+| `503` | 停录未确认或故障;`detail.code` 常见为 `RECORDING_NOT_STOPPED` |
**请求示例**
-```json
+```
{
"surgery_id": "123456"
}
@@ -375,91 +238,78 @@ flowchart LR
**基本信息**
-- 方法:`GET`
-- 路径:`/client/surgeries/{surgery_id}/result`
-- 路径参数:`surgery_id`
-- 请求体:无
+| **项目** | **内容** |
+| -------- | --------------------------------------- |
+| **方法** | `GET` |
+| **路径** | `/client/surgeries/{surgery_id}/result` |
+| **路径参数** | `surgery_id` |
+| **请求体** | 无 |
**业务说明**
-- 仅当存在至少一条消耗明细时返回 `200`
-- 无明细(包括已归档但零消耗)、手术未开始、未成功开录或当前尚不可查时,返回 `503`
-- 上述 `503` 场景的常见错误码为 `RESULT_NOT_READY`
+- 仅当存在至少一条消耗明细时返回 `200`。
+
+- 无明细(包括已归档但零消耗)、手术未开始、未成功开录或当前尚不可查时,返回 `503`。
+
+- 上述 `503` 场景的常见错误码为 `RESULT_NOT_READY`。
+
**响应体(200)**
-- `surgery_id`
- - 类型:`string`
- - 说明:手术号
-- `status`
- - 类型:`string`
- - 说明:成功时通常为 `completed`
-- `message`
- - 类型:`string`
- - 说明:说明
-- `details`
- - 类型:`array`
- - 说明:消耗明细列表,字段见下文
-- `summary`
- - 类型:`array`
- - 说明:按 `item_id` 汇总的结果,字段见下文
+| **字段** | **类型** | **说明** |
+| ------------ | -------- | ----------------------- |
+| `surgery_id` | `string` | 手术号 |
+| `status` | `string` | 成功时通常为 `completed` |
+| `message` | `string` | 说明 |
+| `details` | `array` | 消耗明细列表,字段见下文 |
+| `summary` | `array` | 按 `item_id` 汇总的结果,字段见下文 |
-**`details[]` 元素**
+**`details[]` 元素**
-- `item_id`
- - 类型:`string`
- - 说明:物品 ID;有目录时多为产品编码,否则通常与名称或模型类名一致
-- `item_name`
- - 类型:`string`
- - 说明:物品名称
-- `qty`
- - 类型:`integer`
- - 说明:本条记录数量,当前恒为 `1`;一次识别或一次人工确认只追加一条明细
-- `doctor_id`
- - 类型:`string`
- - 说明:记账关联的医生或系统标识
-- `timestamp`
- - 类型:`string`
- - 说明:ISO 8601 时间(`date-time`)
+| **字段** | **类型** | **说明** |
+| ----------- | --------- | ---------------------------------- |
+| `item_id` | `string` | 物品 ID;有目录时多为产品编码,否则通常与名称或模型类名一致 |
+| `item_name` | `string` | 物品名称 |
+| `qty` | `integer` | 本条记录数量,当前恒为 `1`;一次识别或一次人工确认只追加一条明细 |
+| `doctor_id` | `string` | 记账关联的医生或系统标识 |
+| `timestamp` | `string` | ISO 8601 时间(`date-time`) |
-**`summary[]` 元素**
+**`summary[]` 元素**
-- `item_id`
- - 类型:`string`
- - 说明:与明细一致
-- `item_name`
- - 类型:`string`
- - 说明:名称,通常取该 `item_id` 首条明细中的名称
-- `total_quantity`
- - 类型:`integer`
- - 说明:该物品在本台手术中的合计数量,`>= 0`
+| **字段** | **类型** | **说明** |
+| ---------------- | --------- | -------------------------- |
+| `item_id` | `string` | 与明细一致 |
+| `item_name` | `string` | 名称,通常取该 `item_id` 首条明细中的名称 |
+| `total_quantity` | `integer` | 该物品在本台手术中的合计数量,`>= 0` |
**状态码**
-- `200`:至少有一条明细
-- `422`:`surgery_id` 路径不符合约束
-- `503`:`RESULT_NOT_READY`,当前无可用明细或不可查
+| **HTTP** | **说明** |
+| -------- | ------------------------------ |
+| `200` | 至少有一条明细 |
+| `422` | `surgery_id` 路径不符合约束 |
+| `503` | `RESULT_NOT_READY`,当前无可用明细或不可查 |
**响应示例(200)**
-```json
+```
{
"surgery_id": "123456",
"status": "completed",
"message": "查询成功。",
"details": [
{
- "item_id": "HC001",
- "item_name": "纱布",
+ "item_id": "19246-3-14",
+ "item_name": "医用纱布敷料",
"qty": 1,
- "doctor_id": "D1001",
+ "doctor_id": "6611",
"timestamp": "2026-04-21T10:30:00+08:00"
}
],
"summary": [
{
- "item_id": "HC001",
- "item_name": "纱布",
+ "item_id": "19246-3-14",
+ "item_name": "医用纱布敷料",
"total_quantity": 1
}
]
@@ -470,83 +320,73 @@ flowchart LR
**基本信息**
-- 方法:`GET`
-- 路径:`/client/surgeries/{surgery_id}/pending-confirmation`
-- 路径参数:`surgery_id`
-- 请求体:无
+| **项目** | **内容** |
+| -------- | ----------------------------------------------------- |
+| **方法** | `GET` |
+| **路径** | `/client/surgeries/{surgery_id}/pending-confirmation` |
+| **路径参数** | `surgery_id` |
+| **请求体** | 无 |
**业务说明**
-- 返回当前 FIFO 队首的一条低置信度识别任务
-- `prompt_audio_mp3_base64` 与 `prompt_text` 内容一致,为标准 Base64 的 MP3 字符串(无换行)
-- 客户端解码后应按 `audio/mpeg` 播放
+- 返回当前 FIFO 队首的一条低置信度识别任务。
+
+- `prompt_audio_mp3_base64` 与 `prompt_text` 内容一致,为标准 Base64 的 MP3 字符串(无换行)。
+
+- 客户端解码后应按 `audio/mpeg` 播放。
+
**响应体(200)**
-- `surgery_id`
- - 类型:`string`
- - 说明:手术号
-- `confirmation_id`
- - 类型:`string`
- - 说明:待确认项 ID;提交 5.6 节接口时原样放入路径
-- `prompt_text`
- - 类型:`string`
- - 说明:播报或展示用语,与 MP3 内容一致
-- `prompt_audio_mp3_base64`
- - 类型:`string`
- - 说明:MP3 的 Base64
-- `options`
- - 类型:`array`
- - 说明:候选项列表,字段见下文
-- `model_top1_label`
- - 类型:`string`
- - 说明:模型原始 Top1 类名,可能不在本台候选内
-- `model_top1_confidence`
- - 类型:`number`
- - 说明:Top1 置信度
-- `created_at`
- - 类型:`string`
- - 说明:创建时间(ISO 8601)
+| **字段** | **类型** | **说明** |
+| ------------------------- | -------- | ------------------------- |
+| `surgery_id` | `string` | 手术号 |
+| `confirmation_id` | `string` | 待确认项 ID;提交 5.6 节接口时原样放入路径 |
+| `prompt_text` | `string` | 播报或展示用语,与 MP3 内容一致 |
+| `prompt_audio_mp3_base64` | `string` | MP3 的 Base64 |
+| `options` | `array` | 候选项列表,字段见下文 |
+| `model_top1_label` | `string` | 模型原始 Top1 类名,可能不在本台候选内 |
+| `model_top1_confidence` | `number` | Top1 置信度 |
+| `created_at` | `string` | 创建时间(ISO 8601) |
-**`options[]` 元素**
+**`options[]` 元素**
-- `label`
- - 类型:`string`
- - 说明:展示给医生的选项名称
-- `confidence`
- - 类型:`number`
- - 说明:该选项对应的置信度
+| **字段** | **类型** | **说明** |
+| ------------ | -------- | ---------- |
+| `label` | `string` | 展示给医生的选项名称 |
+| `confidence` | `number` | 该选项对应的置信度 |
**状态码**
-- `200`:当前有一条待确认
-- `404`:无待确认或手术未活跃;常见错误码为 `NO_PENDING_CONFIRMATION`
-- `422`:例如话术为空导致无法 TTS;错误码见响应,如 `TTS_TEXT_EMPTY`
-- `503`:语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED`、`TTS_ERROR`
+| **HTTP** | **说明** |
+| -------- | ----------------------------------------------------- |
+| `200` | 当前有一条待确认 |
+| `404` | 无待确认或手术未活跃;常见错误码为 `NO_PENDING_CONFIRMATION` |
+| `422` | 例如话术为空导致无法 TTS;错误码见响应,如 `TTS_TEXT_EMPTY` |
+| `503` | 语音服务未配置或 TTS 失败;例如 `BAIDU_NOT_CONFIGURED`、`TTS_ERROR` |
### 5.6 提交待确认结果(医生语音)
**基本信息**
-- 方法:`POST`
-- 路径:`/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve`
-- Content-Type:`multipart/form-data`
+| **项目** | **内容** |
+| ---------------- | ------------------------------------------------------------------------------- |
+| **方法** | `POST` |
+| **路径** | `/client/surgeries/{surgery_id}/pending-confirmation/{confirmation_id}/resolve` |
+| **Content-Type** | `multipart/form-data` |
**路径参数**
-- `surgery_id`
- - 约束:6 位数字
- - 说明:同 5.0 节
-- `confirmation_id`
- - 约束:长度 1 到 128
- - 说明:与 5.5 节响应中的 `confirmation_id` 一致
+| **参数** | **约束** | **说明** |
+| ----------------- | ---------- | -------------------------------- |
+| `surgery_id` | 6 位数字 | 同 5.0 节 |
+| `confirmation_id` | 长度 1 到 128 | 与 5.5 节响应中的 `confirmation_id` 一致 |
**请求体(multipart)**
-- `audio`
- - 类型:`file`
- - 必填:是
- - 说明:单个 `.wav` 文件;建议使用 16 kHz 单声道 PCM;非 `.wav` 扩展名会返回 `422`
+| **字段名** | **类型** | **必填** | **说明** |
+| ------- | ------ | ------ | ------------------------------------------------------ |
+| `audio` | `file` | 是 | 单个 `.wav` 文件;建议使用 16 kHz 单声道 PCM;非 `.wav` 扩展名会返回 `422` |
**业务说明**
@@ -554,43 +394,31 @@ flowchart LR
**响应体(200)**
-- `surgery_id`
- - 类型:`string`
- - 说明:手术号
-- `confirmation_id`
- - 类型:`string`
- - 说明:待确认 ID
-- `status`
- - 类型:`string`
- - 说明:成功时为 `accepted`
-- `message`
- - 类型:`string`
- - 说明:说明
-- `resolved_label`
- - 类型:`string | null`
- - 说明:确认后的耗材名称;否认全部候选时为 `null`
-- `rejected`
- - 类型:`boolean`
- - 说明:是否否认全部候选,不记消耗时为 `true`
-- `asr_text`
- - 类型:`string | null`
- - 说明:语音识别文本
-- `audio_object_key`
- - 类型:`string | null`
- - 说明:对象存储中的原始 WAV 键,便于追溯
+| **字段** | **类型** | **说明** |
+| ------------------ | ---------------- | ------------------------ |
+| `surgery_id` | `string` | 手术号 |
+| `confirmation_id` | `string` | 待确认 ID |
+| `status` | `string` | 成功时为 `accepted` |
+| `message` | `string` | 说明 |
+| `resolved_label` | `string \| null` | 确认后的耗材名称;否认全部候选时为 `null` |
+| `rejected` | `boolean` | 是否否认全部候选,不记消耗时为 `true` |
+| `asr_text` | `string \| null` | 语音识别文本 |
+| `audio_object_key` | `string \| null` | 对象存储中的原始 WAV 键,便于追溯 |
**状态码**
-- `200`:已受理并完成解析
-- `404`:确认项不存在或手术未活跃;例如 `CONFIRMATION_NOT_FOUND`
-- `409`:当前确认项已处理过;例如 `CONFIRMATION_ALREADY_RESOLVED`
-- `422`:空文件、非 `.wav`、`VOICE_AUDIO_INVALID`、ASR/解析失败等,具体错误码见响应
-- `503`:MinIO、百度等依赖不可用;例如 `MINIO_NOT_CONFIGURED`、`MINIO_UPLOAD_FAILED`、`BAIDU_NOT_CONFIGURED`
+| **HTTP** | **说明** |
+| -------- | ------------------------------------------------------------------------------------- |
+| `200` | 已受理并完成解析 |
+| `404` | 确认项不存在或手术未活跃;例如 `CONFIRMATION_NOT_FOUND` |
+| `409` | 当前确认项已处理过;例如 `CONFIRMATION_ALREADY_RESOLVED` |
+| `422` | 空文件、非 `.wav`、`VOICE_AUDIO_INVALID`、ASR/解析失败等,具体错误码见响应 |
+| `503` | MinIO、百度等依赖不可用;例如 `MINIO_NOT_CONFIGURED`、`MINIO_UPLOAD_FAILED`、`BAIDU_NOT_CONFIGURED` |
**cURL 示例**
-```bash
+```
curl -sS -X POST \
"http://<主机>:38080/client/surgeries/123456/pending-confirmation//resolve" \
-F "audio=@/path/to/voice.wav;type=audio/wav"
-```
+```
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index a75c5e1..3cc0fdc 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,6 @@ dependencies = [
"chardet>=7.4.3",
"fastapi>=0.136.0",
"loguru>=0.7.3",
- "openpyxl>=3.1.5",
"pillow>=12.2.0",
"pydantic-settings>=2.13.1",
"python-multipart>=0.0.26",
@@ -19,6 +18,7 @@ dependencies = [
"ultralytics>=8.4.40",
"uvicorn[standard]>=0.44.0",
"rich>=15.0.0",
+ "pyyaml>=6.0.3",
]
[project.scripts]
diff --git a/tests/test_effective_candidate_consumables.py b/tests/test_effective_candidate_consumables.py
index 324d172..8966e3a 100644
--- a/tests/test_effective_candidate_consumables.py
+++ b/tests/test_effective_candidate_consumables.py
@@ -1,4 +1,4 @@
-"""effective_candidate_consumables:空请求时回退到目录或模型类名。"""
+"""effective_candidate_consumables / build_name_mapping:YAML 与分类模型,无 Excel。"""
from __future__ import annotations
@@ -6,7 +6,6 @@ from pathlib import Path
from unittest.mock import MagicMock
import pytest
-from openpyxl import Workbook
from app.config import Settings
from app.services.consumable_vision_algorithm import ConsumableVisionAlgorithmService
@@ -18,34 +17,64 @@ def test_effective_preserves_non_empty_request() -> None:
assert got == ["纱布", "缝线"]
-def test_effective_empty_uses_model_class_names(monkeypatch: pytest.MonkeyPatch) -> None:
- svc = ConsumableVisionAlgorithmService(Settings())
+def test_effective_empty_uses_model_when_yaml_has_no_names(
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
+) -> None:
+ yml = tmp_path / "empty.yaml"
+ yml.write_text("names: {}\nlabel_id: {}\n", encoding="utf-8")
+ s = Settings(consumable_classifier_labels_yaml_path=str(yml))
+ svc = ConsumableVisionAlgorithmService(s)
mock_cls = MagicMock()
mock_cls.names = {0: "ban", 1: "apple"}
monkeypatch.setattr(svc, "_get_cls", lambda: mock_cls)
assert svc.effective_candidate_consumables([]) == ["apple", "ban"]
-def test_effective_empty_prefers_catalog_xlsx(tmp_path: Path) -> None:
- xlsx = tmp_path / "cat.xlsx"
- wb = Workbook()
- ws = wb.active
- ws.append(["产品编码", "商品名称"])
- ws.append(["C1", "商品乙"])
- ws.append(["C2", "商品甲"])
- wb.save(xlsx)
-
- settings = Settings(consumable_catalog_xlsx_path=str(xlsx))
- svc = ConsumableVisionAlgorithmService(settings)
- got = svc.effective_candidate_consumables([])
- assert got == ["商品乙", "商品甲"]
+def test_effective_empty_prefers_yaml_class_names(tmp_path: Path) -> None:
+ yml = tmp_path / "lab.yaml"
+ yml.write_text(
+ "names:\n 0: 商品甲\n 1: 商品乙\nlabel_id:\n 0: a\n 1: b\n",
+ encoding="utf-8",
+ )
+ s = Settings(consumable_classifier_labels_yaml_path=str(yml))
+ svc = ConsumableVisionAlgorithmService(s)
+ assert svc.effective_candidate_consumables([]) == ["商品甲", "商品乙"]
def test_effective_whitespace_only_treated_as_empty(
- monkeypatch: pytest.MonkeyPatch,
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
) -> None:
- svc = ConsumableVisionAlgorithmService(Settings())
+ yml = tmp_path / "empty.yaml"
+ yml.write_text("names: {}\nlabel_id: {}\n", encoding="utf-8")
+ s = Settings(consumable_classifier_labels_yaml_path=str(yml))
+ svc = ConsumableVisionAlgorithmService(s)
mock_cls = MagicMock()
mock_cls.names = {0: "x"}
monkeypatch.setattr(svc, "_get_cls", lambda: mock_cls)
assert svc.effective_candidate_consumables(["", " "]) == ["x"]
+
+
+def test_build_name_mapping_from_label_id(tmp_path: Path) -> None:
+ yml = tmp_path / "lab.yaml"
+ yml.write_text(
+ "names:\n 0: 商品A\nlabel_id:\n 0: y1/y2\n",
+ encoding="utf-8",
+ )
+ s = Settings(consumable_classifier_labels_yaml_path=str(yml))
+ svc = ConsumableVisionAlgorithmService(s)
+ m = svc.build_name_mapping(["商品A"])
+ assert m["商品A"] == "y1/y2"
+
+
+def test_build_name_mapping_uses_name_when_no_id_in_yaml(
+ tmp_path: Path,
+) -> None:
+ yml = tmp_path / "lab.yaml"
+ yml.write_text(
+ "names:\n 0: 仅表内有的\nlabel_id: {}\n",
+ encoding="utf-8",
+ )
+ s = Settings(consumable_classifier_labels_yaml_path=str(yml))
+ svc = ConsumableVisionAlgorithmService(s)
+ m = svc.build_name_mapping(["仅表内有的"])
+ assert m["仅表内有的"] == "仅表内有的"
diff --git a/uv.lock b/uv.lock
index f441e58..a774c76 100644
--- a/uv.lock
+++ b/uv.lock
@@ -302,7 +302,7 @@ name = "colorama"
version = "0.4.6"
source = { registry = "https://download.pytorch.org/whl/cpu" }
wheels = [
- { url = "https://download.pytorch.org/whl/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6" },
+ { url = "https://download.pytorch.org/whl/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", upload-time = "2023-10-05T23:50:34Z" },
]
[[package]]
@@ -369,15 +369,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
]
-[[package]]
-name = "et-xmlfile"
-version = "2.0.0"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" },
-]
-
[[package]]
name = "fastapi"
version = "0.136.0"
@@ -567,7 +558,7 @@ dependencies = [
{ name = "markupsafe" },
]
wheels = [
- { url = "https://download.pytorch.org/whl/jinja2-3.1.6-py3-none-any.whl" },
+ { url = "https://download.pytorch.org/whl/jinja2-3.1.6-py3-none-any.whl", upload-time = "2025-10-14T18:38:59Z" },
]
[[package]]
@@ -679,22 +670,22 @@ name = "markupsafe"
version = "3.0.3"
source = { registry = "https://download.pytorch.org/whl/cpu" }
wheels = [
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-win_amd64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-win_amd64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl" },
- { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", upload-time = "2026-03-27T13:54:35Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = "2026-03-27T13:54:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = "2026-03-27T13:54:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", upload-time = "2026-03-27T13:54:39Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", upload-time = "2026-03-27T13:54:39Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = "2026-03-27T13:54:39Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = "2026-03-27T13:54:41Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", upload-time = "2026-03-27T13:54:41Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", upload-time = "2026-02-23T16:46:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = "2026-02-23T16:46:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = "2026-02-23T16:46:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", upload-time = "2026-02-23T16:46:37Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", upload-time = "2026-02-23T16:46:39Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", upload-time = "2026-02-23T16:46:41Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", upload-time = "2026-02-23T16:46:41Z" },
+ { url = "https://download.pytorch.org/whl/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", upload-time = "2026-02-23T16:46:41Z" },
]
[[package]]
@@ -855,18 +846,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" },
]
-[[package]]
-name = "openpyxl"
-version = "3.1.5"
-source = { registry = "https://pypi.org/simple" }
-dependencies = [
- { name = "et-xmlfile" },
-]
-sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
-]
-
[[package]]
name = "operation-room-monitor-server"
version = "0.1.0"
@@ -879,10 +858,10 @@ dependencies = [
{ name = "greenlet" },
{ name = "loguru" },
{ name = "minio" },
- { name = "openpyxl" },
{ name = "pillow" },
{ name = "pydantic-settings" },
{ name = "python-multipart" },
+ { name = "pyyaml" },
{ name = "rich" },
{ name = "sqlalchemy" },
{ name = "ultralytics" },
@@ -907,10 +886,10 @@ requires-dist = [
{ name = "greenlet", specifier = ">=3.1.0" },
{ name = "loguru", specifier = ">=0.7.3" },
{ name = "minio", specifier = ">=7.2.15" },
- { name = "openpyxl", specifier = ">=3.1.5" },
{ name = "pillow", specifier = ">=12.2.0" },
{ name = "pydantic-settings", specifier = ">=2.13.1" },
{ name = "python-multipart", specifier = ">=0.0.26" },
+ { name = "pyyaml", specifier = ">=6.0.3" },
{ name = "rich", specifier = ">=15.0.0" },
{ name = "sqlalchemy", specifier = ">=2.0.49" },
{ name = "ultralytics", specifier = ">=8.4.40" },
@@ -1472,10 +1451,10 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform == 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:442ec9dc78592564fdad69cf0beaa9da2f82ab810ccb4f13903869a90bf3f15d", upload-time = "2026-03-23T15:17:02Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cc3a195701bba2239c313ee311487f80f8aaebe9e89b9073dddbcf2f93b5a0ba", upload-time = "2026-03-23T15:17:06Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:072a0d6e4865e8b0dc0dbfe6ebed68fae235124222835ef03e5814d414d8c012", upload-time = "2026-03-23T15:17:10Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:23ec7789017da9d95b6d543d790814785e6f30905c5443efa8257d1490d73f79", upload-time = "2026-03-23T15:17:14Z" },
]
[[package]]
@@ -1498,22 +1477,22 @@ dependencies = [
{ name = "typing-extensions", marker = "sys_platform != 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-linux_s390x.whl", upload-time = "2026-03-23T14:59:04Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:04Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:05Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313-win_amd64.whl", upload-time = "2026-03-23T14:59:06Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-linux_s390x.whl", upload-time = "2026-03-23T14:59:07Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:07Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:07Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp313-cp313t-win_amd64.whl", upload-time = "2026-03-23T14:59:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-linux_s390x.whl", upload-time = "2026-03-23T14:59:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:10Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:11Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314-win_amd64.whl", upload-time = "2026-03-23T14:59:12Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-linux_s390x.whl", upload-time = "2026-03-23T14:59:12Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", upload-time = "2026-03-23T14:59:12Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", upload-time = "2026-03-23T14:59:13Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torch-2.11.0%2Bcpu-cp314-cp314t-win_amd64.whl", upload-time = "2026-03-23T14:59:15Z" },
]
[[package]]
@@ -1530,10 +1509,10 @@ dependencies = [
{ name = "torch", version = "2.11.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5d63dd43162691258b1b3529b9041bac7d54caa37eae0925f997108268cbf7c4", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:358fc4726d0c08615b6d83b3149854f11efb2a564ed1acb6fce882e151412d23", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:eb61804eb9dbe88c5a2a6c4da8dec1d80d2d0a6f18c999c524e32266cb1ebcd3", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:b7d3e295624a28b3b1769228ce1345d94cf4d390dd31136766f76f2d20f718da", upload-time = "2026-03-23T15:36:09Z" },
]
[[package]]
@@ -1552,18 +1531,18 @@ dependencies = [
{ name = "torch", version = "2.11.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" },
]
wheels = [
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e932af123a39137815dfd152c64cc683fa7cbd327c965e807c9728c7aa4971a" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:16c4f11eda096dc377e82238c8ebb26c7013622c0f1b2c4dcf85fc70f96c0ea7" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:34ac55a1f614baca2e0f5cef20ddb36184ee3503423871260e1ddd72caf9cb5f" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3d30ce3444698807d4b18b199645cd7a95e0b16a4cd0909b8aab47c562a7673a" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:870a97101168d4da68039d3d51f0c781047065e82dc4c19b2eb0ddff08486180" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:050aaf28cff9c2981ec72dc3f9b4ef77bcf9c9c99330ce426cb06c5bb9e6e726" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:78576c8d5a8665de6caaa6e7c3a3fb7caa5dc112032ba60e129a9e78a446a03b" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:78e88d0a57bfadcd17042aa92fe4dd1059e48fcaa2e54a10ac7f438c2eca10d5" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:93144d0997c51b27996c8305df4d9104efb0d38c9a9b6b05c8bc20ebdf7193b5" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:93a11b159613ad920b1d42c4eb4e585f48e5dff895f3e08f517ef482fe84e130" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:99f86ec0a83b9e4b5428a452bf667f99a9ae27d4c32bd4b2081fe917303e7710" },
- { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:6139108231a29ffb607931360ee24594553a939467c65530f734a2ed9918f011" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:2e932af123a39137815dfd152c64cc683fa7cbd327c965e807c9728c7aa4971a", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:16c4f11eda096dc377e82238c8ebb26c7013622c0f1b2c4dcf85fc70f96c0ea7", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:34ac55a1f614baca2e0f5cef20ddb36184ee3503423871260e1ddd72caf9cb5f", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:3d30ce3444698807d4b18b199645cd7a95e0b16a4cd0909b8aab47c562a7673a", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:870a97101168d4da68039d3d51f0c781047065e82dc4c19b2eb0ddff08486180", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:050aaf28cff9c2981ec72dc3f9b4ef77bcf9c9c99330ce426cb06c5bb9e6e726", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:78576c8d5a8665de6caaa6e7c3a3fb7caa5dc112032ba60e129a9e78a446a03b", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:78e88d0a57bfadcd17042aa92fe4dd1059e48fcaa2e54a10ac7f438c2eca10d5", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314-win_amd64.whl", hash = "sha256:93144d0997c51b27996c8305df4d9104efb0d38c9a9b6b05c8bc20ebdf7193b5", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:93a11b159613ad920b1d42c4eb4e585f48e5dff895f3e08f517ef482fe84e130", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:99f86ec0a83b9e4b5428a452bf667f99a9ae27d4c32bd4b2081fe917303e7710", upload-time = "2026-03-23T15:36:09Z" },
+ { url = "https://download-r2.pytorch.org/whl/cpu/torchvision-0.26.0%2Bcpu-cp314-cp314t-win_amd64.whl", hash = "sha256:6139108231a29ffb607931360ee24594553a939467c65530f734a2ed9918f011", upload-time = "2026-03-23T15:36:09Z" },
]
[[package]]