feat: align surgery API with schemas and extend client tooling
- Refactor app API and schemas; adjust surgery pipeline, repository, and session manager. - Improve consumption TSV logging and consumable vision integration; trim voice resolution. - Add Baidu Face 1:N search script, .env.example entries, and client API integration doc. - Update demo client, staging checklist, surgery interface doc, and related tests; add sample face image. Made-with: Cursor
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
"""每例手术一个文本文件(制表符列):`start_surgery` 时截断并写表头,每次时间窗识别**追加**一行。终端 Markdown 中时间戳为可读形式;落盘行内仍为 ISO 便于程序解析。
|
||||
"""每例手术一个文本文件(制表符列):`start_surgery` 时截断并写表头,每次时间窗识别**追加**一行(仅 item_id, item_name, qty, doctor_id, timestamp)。终端 Markdown 时间戳为可读形式;落盘时间戳为 ISO 区间便于程序解析。
|
||||
|
||||
手术结束时再追加一节汇总行:item_id, item_name, qty(无其它列)。
|
||||
|
||||
时间戳:在拉流起点记录 `time.time()`,与 `time.monotonic()` 时间窗对齐。直播 RTSP 经 OpenCV 一般无可靠绝对时码,以本机接收时刻为准。
|
||||
"""
|
||||
@@ -14,11 +16,12 @@ from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
from loguru import logger
|
||||
|
||||
from app.config import settings
|
||||
from app.services.consumable_vision_algorithm import ClsTop3
|
||||
from app.services.consumable_vision_algorithm import ClsTop3, _norm_product_name
|
||||
from app.terminal_markdown import print_markdown_stderr
|
||||
|
||||
# 制表符分隔;时间范围用 U+2013 连接;Top2/3 仅名称;本窗消耗数量恒为 1
|
||||
HEADER = "物品id\t物品名称\tTop2物品名称\tTop3物品名称\t消耗数量\t医生id\t时间戳\n"
|
||||
# 制表符分隔;时间范围用 U+2013 连接;本窗消耗数量恒为 1
|
||||
HEADER = "item_id\titem_name\tqty\tdoctor_id\ttimestamp\n"
|
||||
SUMMARY_HEADER = "item_id\titem_name\tqty\n"
|
||||
_RANGE_SEP = "\u2013" # en dash,与样例 `00:00:00.000–00:00:45.000` 一致
|
||||
|
||||
_lock = threading.Lock()
|
||||
@@ -83,13 +86,20 @@ def _encode_cell(value: str) -> str:
|
||||
return s
|
||||
|
||||
|
||||
def _item_id_for_row(name: str, pid: str, name_to_code: dict[str, str]) -> str:
|
||||
p = (pid or "").strip()
|
||||
def resolve_consumption_item_id(
|
||||
t1_name: str,
|
||||
t1_pid: str,
|
||||
name_to_code: dict[str, str],
|
||||
) -> str:
|
||||
"""业务物品 id:`name_to_code` 的键为归一化名称,须与分类输出一同参与查找。"""
|
||||
n = (t1_name or "").strip()
|
||||
norm = _norm_product_name(n)
|
||||
code = (name_to_code.get(norm) or name_to_code.get(n) or "").strip()
|
||||
if code:
|
||||
return code
|
||||
p = (t1_pid or "").strip()
|
||||
if p:
|
||||
return p
|
||||
n = (name or "").strip()
|
||||
if n in name_to_code:
|
||||
return (name_to_code.get(n) or n).strip()
|
||||
return n
|
||||
|
||||
|
||||
@@ -102,17 +112,12 @@ def build_tsv_line(
|
||||
wall_start_epoch: float,
|
||||
wall_end_epoch: float,
|
||||
) -> str:
|
||||
id1 = _item_id_for_row(best.t1_name, best.t1_pid, name_to_code)
|
||||
# 与历史样例:Top1 为「名称 置信度」四位小数
|
||||
name1 = f"{(best.t1_name or '').strip()} {best.t1_conf:.4f}".strip()
|
||||
n2 = (best.t2_name or "").strip()
|
||||
n3 = (best.t3_name or "").strip()
|
||||
id1 = resolve_consumption_item_id(best.t1_name, best.t1_pid, name_to_code)
|
||||
name1 = (best.t1_name or "").strip()
|
||||
ts = format_consumption_timestamp(camera_id, wall_start_epoch, wall_end_epoch)
|
||||
row = [
|
||||
_encode_cell(id1),
|
||||
_encode_cell(name1),
|
||||
_encode_cell(n2),
|
||||
_encode_cell(n3),
|
||||
"1",
|
||||
_encode_cell(doctor_id),
|
||||
_encode_cell(ts),
|
||||
@@ -179,25 +184,17 @@ def build_consumption_markdown(
|
||||
wall_start_epoch: float,
|
||||
wall_end_epoch: float,
|
||||
) -> str:
|
||||
"""终端用:Top1 含 id/名称/置信度;Top2/3 仅名称;消耗数量恒为 1。"""
|
||||
id1 = _item_id_for_row(best.t1_name, best.t1_pid, name_to_code)
|
||||
"""终端用:与落盘列一致;本窗 qty 恒为 1。"""
|
||||
id1 = resolve_consumption_item_id(best.t1_name, best.t1_pid, name_to_code)
|
||||
n1 = (best.t1_name or "").strip()
|
||||
has2 = bool((best.t2_name or "").strip())
|
||||
has3 = bool((best.t3_name or "").strip())
|
||||
n2 = (best.t2_name or "").strip() if has2 else ""
|
||||
n3 = (best.t3_name or "").strip() if has3 else ""
|
||||
dash = "—"
|
||||
ts = format_consumption_timestamp_readable(camera_id, wall_start_epoch, wall_end_epoch)
|
||||
return "\n".join(
|
||||
[
|
||||
"| Top1 物品id | Top1 物品名称 | Top1 置信度 | Top2 物品名称 | Top3 物品名称 | 消耗数量 | 医生id | 时间戳 |",
|
||||
"| :--- | :--- | ---: | :--- | :--- | ---: | :--- | :--- |",
|
||||
"| {} | {} | {:.4f} | {} | {} | 1 | {} | {} |".format(
|
||||
"| item_id | item_name | qty | doctor_id | timestamp |",
|
||||
"| :--- | :--- | ---: | :--- | :--- |",
|
||||
"| {} | {} | 1 | {} | {} |".format(
|
||||
_md_cell(id1),
|
||||
_md_cell(n1),
|
||||
best.t1_conf,
|
||||
_md_cell(n2) if has2 else dash,
|
||||
_md_cell(n3) if has3 else dash,
|
||||
_md_cell(doctor_id),
|
||||
_md_cell(ts),
|
||||
),
|
||||
@@ -206,6 +203,47 @@ def build_consumption_markdown(
|
||||
)
|
||||
|
||||
|
||||
def append_consumption_log_summary(
|
||||
surgery_id: str,
|
||||
totals: dict[str, tuple[str, int]],
|
||||
) -> None:
|
||||
"""在明细行之后追加汇总块(表头 + 每物品一行)。"""
|
||||
if not settings.consumption_tsv_log_enabled or not totals:
|
||||
return
|
||||
path = resolved_consumption_log_path(surgery_id)
|
||||
if not path.is_file():
|
||||
return
|
||||
body = "".join(
|
||||
["\n", SUMMARY_HEADER]
|
||||
+ [
|
||||
"\t".join([_encode_cell(iid), _encode_cell(name), str(qty)]) + "\n"
|
||||
for iid, (name, qty) in sorted(totals.items(), key=lambda x: x[0])
|
||||
]
|
||||
)
|
||||
with _lock:
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(body)
|
||||
|
||||
|
||||
def print_consumption_summary_markdown(
|
||||
totals: dict[str, tuple[str, int]],
|
||||
) -> None:
|
||||
if not settings.consumption_log_markdown_terminal or not totals:
|
||||
return
|
||||
lines = [
|
||||
"## 消耗汇总",
|
||||
"",
|
||||
"| item_id | item_name | qty |",
|
||||
"| :--- | :--- | ---: |",
|
||||
]
|
||||
for iid, (name, qty) in sorted(totals.items(), key=lambda x: x[0]):
|
||||
lines.append(
|
||||
"| {} | {} | {} |".format(_md_cell(iid), _md_cell(name), qty)
|
||||
)
|
||||
lines.append("")
|
||||
print_markdown_stderr("\n".join(lines))
|
||||
|
||||
|
||||
def append_consumption_window(
|
||||
*,
|
||||
surgery_id: str,
|
||||
@@ -215,9 +253,17 @@ def append_consumption_window(
|
||||
camera_id: str,
|
||||
wall_start_epoch: float,
|
||||
wall_end_epoch: float,
|
||||
running_totals: dict[str, tuple[str, int]] | None = None,
|
||||
) -> None:
|
||||
if not settings.consumption_tsv_log_enabled and not settings.consumption_log_markdown_terminal:
|
||||
return
|
||||
iid = resolve_consumption_item_id(best.t1_name, best.t1_pid, name_to_code)
|
||||
iname = (best.t1_name or "").strip()
|
||||
if running_totals is not None:
|
||||
if iid not in running_totals:
|
||||
running_totals[iid] = (iname, 0)
|
||||
prev_name, q = running_totals[iid]
|
||||
running_totals[iid] = (prev_name, q + 1)
|
||||
if settings.consumption_tsv_log_enabled:
|
||||
line = build_tsv_line(
|
||||
name_to_code=name_to_code,
|
||||
|
||||
Reference in New Issue
Block a user