feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import re
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_CN_DIGITS = {
|
|
|
|
|
|
"零": 0,
|
|
|
|
|
|
"一": 1,
|
|
|
|
|
|
"二": 2,
|
|
|
|
|
|
"两": 2,
|
|
|
|
|
|
"三": 3,
|
|
|
|
|
|
"四": 4,
|
|
|
|
|
|
"五": 5,
|
|
|
|
|
|
"六": 6,
|
|
|
|
|
|
"七": 7,
|
|
|
|
|
|
"八": 8,
|
|
|
|
|
|
"九": 9,
|
|
|
|
|
|
"十": 10,
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-23 14:24:20 +08:00
|
|
|
|
def _parse_ordinal_index_1based(token: str) -> int | None:
|
|
|
|
|
|
"""将「1」「3」「一」「三」「十一」等解析为 1-based 序数,失败返回 None。"""
|
|
|
|
|
|
t = (token or "").strip()
|
|
|
|
|
|
if not t:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if t.isdigit():
|
|
|
|
|
|
v = int(t)
|
|
|
|
|
|
return v if 1 <= v <= 99 else None
|
|
|
|
|
|
if t in _CN_DIGITS and t != "零" and t != "十":
|
|
|
|
|
|
return int(_CN_DIGITS[t])
|
|
|
|
|
|
if t == "十":
|
|
|
|
|
|
return 10
|
|
|
|
|
|
if len(t) == 2 and t[0] == "十" and t[1] in _CN_DIGITS and t[1] not in ("零", "十"):
|
|
|
|
|
|
return 10 + int(_CN_DIGITS[t[1]])
|
|
|
|
|
|
if len(t) == 2 and t[1] == "十" and t[0] in _CN_DIGITS and t[0] != "零":
|
|
|
|
|
|
return int(_CN_DIGITS[t[0]]) * 10
|
|
|
|
|
|
if len(t) == 3 and t[0] in _CN_DIGITS and t[1] == "十" and t[2] in _CN_DIGITS:
|
|
|
|
|
|
return int(_CN_DIGITS[t[0]]) * 10 + int(_CN_DIGITS[t[2]])
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _label_from_ordinal_1based(n1: int, options: list[str]) -> str | None:
|
|
|
|
|
|
if n1 < 1:
|
|
|
|
|
|
return None
|
|
|
|
|
|
idx = n1 - 1
|
|
|
|
|
|
if 0 <= idx < len(options):
|
|
|
|
|
|
return options[idx]
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _choose_from_ordinal_text(raw: str, options: list[str]) -> str | None:
|
|
|
|
|
|
"""从「第一个」「第2个」「选3」「1号」等表述解析选项。返回 None 表示本函数未识别。"""
|
|
|
|
|
|
n_opt = len(options)
|
|
|
|
|
|
if n_opt < 1:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 1) 显式「第N个/项/款/…」,允许夹带后噪声,如「第一个对」
|
|
|
|
|
|
for m in re.finditer(
|
|
|
|
|
|
r"第([0-9]+|[一二两三四五六七八九十百]+)(?:个|项|款|的|种|名)?", raw
|
|
|
|
|
|
):
|
|
|
|
|
|
n1 = _parse_ordinal_index_1based(m.group(1))
|
|
|
|
|
|
if n1 is not None:
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
|
|
|
|
|
m_pick = re.search(
|
|
|
|
|
|
r"(?:^|[\s,,;;::])(?:选|要|就)\s*0*([1-9]\d?)(?:\s*号|个|项|款)?",
|
|
|
|
|
|
raw,
|
|
|
|
|
|
)
|
|
|
|
|
|
if m_pick:
|
|
|
|
|
|
n1 = int(m_pick.group(1))
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
|
|
|
|
|
norm_for_opt = raw.replace(" ", "").lower()
|
|
|
|
|
|
m_op = re.search(r"(?:option|选项)\s*[::]?\s*(\d+)", norm_for_opt, re.IGNORECASE)
|
|
|
|
|
|
if m_op:
|
|
|
|
|
|
n1 = int(m_op.group(1))
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
|
|
|
|
|
|
|
|
|
|
|
# 2) 行首/句末「一」「二」单字,仅当候选项数较少时
|
|
|
|
|
|
s = raw.replace(" ", "")
|
|
|
|
|
|
if n_opt <= 3:
|
|
|
|
|
|
m_one = re.match(r"^([一二两三四])$", s)
|
|
|
|
|
|
if m_one:
|
|
|
|
|
|
tok = m_one.group(1)
|
|
|
|
|
|
if tok in _CN_DIGITS and tok not in ("零", "十"):
|
|
|
|
|
|
n1 = int(_CN_DIGITS[tok])
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
|
|
|
|
|
m_tail = re.search(r"([0-9一二两三四五六七八九十]+)\s*号$", s)
|
|
|
|
|
|
if m_tail:
|
|
|
|
|
|
n1 = _parse_ordinal_index_1based(m_tail.group(1))
|
|
|
|
|
|
if n1 is not None:
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
def parse_voice_choice(asr_text: str, options: list[str]) -> str | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
从识别文本中解析医生选择的耗材名称。
|
|
|
|
|
|
支持:完全匹配、子串匹配、第 N 个(1/一/第一个)。
|
|
|
|
|
|
"""
|
2026-04-23 14:24:20 +08:00
|
|
|
|
raw = re.sub(
|
|
|
|
|
|
r"^[。,、;:!?\s]+|[。,、;:!?\s]+$",
|
|
|
|
|
|
"",
|
|
|
|
|
|
(asr_text or "").strip(),
|
|
|
|
|
|
)
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
if not raw:
|
|
|
|
|
|
return None
|
|
|
|
|
|
normalized = raw.replace(" ", "").lower()
|
|
|
|
|
|
|
|
|
|
|
|
for opt in options:
|
|
|
|
|
|
if opt and opt in raw:
|
|
|
|
|
|
return opt
|
|
|
|
|
|
|
2026-04-23 14:24:20 +08:00
|
|
|
|
chosen_ord = _choose_from_ordinal_text(raw, options)
|
|
|
|
|
|
if chosen_ord is not None:
|
|
|
|
|
|
return chosen_ord
|
|
|
|
|
|
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
m_num = re.search(r"(\d+)", raw)
|
|
|
|
|
|
if m_num:
|
|
|
|
|
|
idx = int(m_num.group(1)) - 1
|
|
|
|
|
|
if 0 <= idx < len(options):
|
|
|
|
|
|
return options[idx]
|
|
|
|
|
|
|
|
|
|
|
|
m_cn = re.search(r"第([一二两三四五六七八九十\d]+)个", raw)
|
|
|
|
|
|
if m_cn:
|
|
|
|
|
|
token = m_cn.group(1)
|
2026-04-23 14:24:20 +08:00
|
|
|
|
n1 = int(token) if token.isdigit() else _parse_ordinal_index_1based(token)
|
|
|
|
|
|
if n1 is not None:
|
|
|
|
|
|
ch = _label_from_ordinal_1based(n1, options)
|
|
|
|
|
|
if ch is not None:
|
|
|
|
|
|
return ch
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
|
|
|
|
|
|
for i, opt in enumerate(options):
|
|
|
|
|
|
if not opt:
|
|
|
|
|
|
continue
|
|
|
|
|
|
aliases = [f"第{i + 1}个", f"第{i + 1}", f"{i + 1}号"]
|
|
|
|
|
|
if any(a in normalized for a in aliases):
|
|
|
|
|
|
return opt
|
|
|
|
|
|
|
|
|
|
|
|
negatives = ("不是", "没有", "否", "无", "错")
|
|
|
|
|
|
if any(n in raw for n in negatives):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-04-22 16:31:12 +08:00
|
|
|
|
def match_voice_choice_against_candidates(
|
|
|
|
|
|
asr_text: str, candidates: list[str]
|
|
|
|
|
|
) -> str | None:
|
|
|
|
|
|
"""
|
|
|
|
|
|
在未匹配 pending 展示的 topk 话术时,按本台手术「候选耗材清单」做名称子串匹配。
|
|
|
|
|
|
长名优先,减少短名误命中(如「纱」同时匹配多种耗材时优先更长全称)。
|
|
|
|
|
|
"""
|
|
|
|
|
|
raw = (asr_text or "").strip()
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return None
|
|
|
|
|
|
stripped = [c.strip() for c in candidates if c and str(c).strip()]
|
|
|
|
|
|
if not stripped:
|
|
|
|
|
|
return None
|
|
|
|
|
|
for c in sorted(stripped, key=len, reverse=True):
|
|
|
|
|
|
if c in raw:
|
|
|
|
|
|
return c
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
def is_rejection_phrase(asr_text: str) -> bool:
|
|
|
|
|
|
"""医生明确否认全部候选时返回 True(须在 parse_voice_choice 之前调用)。"""
|
|
|
|
|
|
raw = (asr_text or "").strip()
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return False
|
|
|
|
|
|
negatives = ("不是", "没有", "否", "无", "错")
|
|
|
|
|
|
return any(n in raw for n in negatives)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def build_prompt_text(options: list[tuple[str, float]]) -> str:
|
2026-04-23 14:24:20 +08:00
|
|
|
|
parts = ["请确认刚才使用的耗材是下面哪一项。"]
|
feat: surgery pipeline API, video inference, voice confirm, and tests
- Add FastAPI routes for surgery start/end, results, pending confirmation (WAV upload), and health checks.
- Implement RTSP/Hikvision capture, consumable classification, session manager, MinIO/Baidu voice resolution, and DB persistence.
- Add documentation (client API, video backends, staging checklist) and sample camera/RTSP config.
- Add pytest suite (API contract, session manager, voice, repositories, pipeline persistence) and httpx dev dependency.
- Replace deprecated HTTP_422_UNPROCESSABLE_ENTITY with HTTP_422_UNPROCESSABLE_CONTENT.
- Fix SurgeryPipeline DB reads to use an explicit transaction with autobegin disabled.
Made-with: Cursor
2026-04-21 18:33:54 +08:00
|
|
|
|
for i, (name, _conf) in enumerate(options, start=1):
|
|
|
|
|
|
parts.append(f"第{i}个,{name}。")
|
|
|
|
|
|
return "".join(parts)
|