更新待学习库,和判断运动更新,
This commit is contained in:
parent
3e09e771cf
commit
2bf6d2c47f
|
|
@ -131,8 +131,9 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._live_stable_committed_sig = None
|
self._live_stable_committed_sig = None
|
||||||
self._live_stable_snapshot = None # 稳定期间保留的 dishes,供写入
|
self._live_stable_snapshot = None # 稳定期间保留的 dishes,供写入
|
||||||
self._last_reported_sig = None # 实时上报去重:相同结果只报一次
|
self._last_reported_sig = None # 实时上报去重:相同结果只报一次
|
||||||
self._last_detect_fp = None # 识别后锚点指纹(静止等待运动)
|
self._last_detect_fp = None # 识别后锚点指纹(静止等待运动;仅餐盘区域)
|
||||||
self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时)
|
self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时)
|
||||||
|
self._last_motion_boxes = None # 上一轮餐盘框,运动判定只看菜品区域
|
||||||
self._frame_sim_threshold = 0.992 # 帧相似度阈值:判定静止 / 运动
|
self._frame_sim_threshold = 0.992 # 帧相似度阈值:判定静止 / 运动
|
||||||
# 运动状态机:动→静保持 still_hold 秒 → YOLO 一次 → 静待运动 → 再静…
|
# 运动状态机:动→静保持 still_hold 秒 → YOLO 一次 → 静待运动 → 再静…
|
||||||
self._live_still_hold_seconds = float(DISH_LIVE_STILL_HOLD_SECONDS)
|
self._live_still_hold_seconds = float(DISH_LIVE_STILL_HOLD_SECONDS)
|
||||||
|
|
@ -194,9 +195,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def take_live_frame(self):
|
def take_live_frame(self):
|
||||||
"""抓取实时帧;返回 (0, path, frame) 或 (-1, None, None)。
|
"""抓取实时帧(默认不落盘)。返回 (0, None, frame) 或 (-1, None, None)。
|
||||||
|
|
||||||
优先复用预览缓存帧,避免与预览争抢相机;磁盘路径供上报/待学习。
|
优先复用预览缓存帧,避免与预览争抢相机。
|
||||||
|
有订单时再调用 _save_order_image 写入 images/。
|
||||||
"""
|
"""
|
||||||
frame = None
|
frame = None
|
||||||
with self._live_lock:
|
with self._live_lock:
|
||||||
|
|
@ -209,19 +211,25 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
frame = self.grab_preview_frame()
|
frame = self.grab_preview_frame()
|
||||||
if frame is None:
|
if frame is None:
|
||||||
return -1, None, None
|
return -1, None, None
|
||||||
|
return 0, None, frame
|
||||||
|
|
||||||
|
def _save_order_image(self, frame_bgr):
|
||||||
|
"""有一单时落盘一张整帧到 images/;成功返回绝对路径,失败返回 None。"""
|
||||||
|
if frame_bgr is None:
|
||||||
|
return None
|
||||||
os.makedirs("images", exist_ok=True)
|
os.makedirs("images", exist_ok=True)
|
||||||
stamp = int(time.time() * 1000)
|
stamp = int(time.time() * 1000)
|
||||||
save_path = os.path.abspath(os.path.join("images", f"_live_frame_{stamp}.jpg"))
|
save_path = os.path.abspath(os.path.join("images", f"_live_frame_{stamp}.jpg"))
|
||||||
try:
|
try:
|
||||||
if cv2.imwrite(save_path, frame):
|
if cv2.imwrite(save_path, frame_bgr):
|
||||||
self._cleanup_old_live_frames(keep=30)
|
self._cleanup_old_live_frames(keep=3000000)
|
||||||
return 0, save_path, frame
|
return save_path
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"保存实时帧失败: {e}")
|
print(f"保存订单帧失败: {e}")
|
||||||
return -1, None, None
|
return None
|
||||||
|
|
||||||
def _cleanup_old_live_frames(self, keep=30):
|
def _cleanup_old_live_frames(self, keep=30):
|
||||||
"""清理过期实时帧,防止 images/ 无限增长。"""
|
"""清理过期订单帧,防止 images/ 无限增长。"""
|
||||||
try:
|
try:
|
||||||
folder = os.path.abspath("images")
|
folder = os.path.abspath("images")
|
||||||
files = []
|
files = []
|
||||||
|
|
@ -1850,17 +1858,26 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._motion_still_since = None
|
self._motion_still_since = None
|
||||||
self._yolo_trigger_pending = False
|
self._yolo_trigger_pending = False
|
||||||
|
|
||||||
|
def _motion_fingerprint(self, frame_bgr):
|
||||||
|
"""只对菜品/餐盘区域提指纹,忽略旁边行人晃动。"""
|
||||||
|
return self._frame_fingerprint(
|
||||||
|
frame_bgr,
|
||||||
|
boxes=getattr(self, "_last_motion_boxes", None),
|
||||||
|
)
|
||||||
|
|
||||||
def _update_motion_gate(self, frame_bgr):
|
def _update_motion_gate(self, frame_bgr):
|
||||||
"""根据当前帧更新运动状态;从动到静保持 hold 秒时置位 YOLO 触发。
|
"""根据当前帧更新运动状态;从动到静保持 hold 秒时置位 YOLO 触发。
|
||||||
|
|
||||||
|
运动只看餐盘区域(有上次检测框用框,否则画面中央),避免旁人晃动误触发。
|
||||||
|
|
||||||
状态:
|
状态:
|
||||||
moving — 画面在动(或刚开扫)
|
moving — 菜品区域在动(或刚开扫)
|
||||||
settling — 刚停住,累计静止时间
|
settling — 菜品刚停住,累计静止时间
|
||||||
wait_motion — 已识别过,静止中忽略,直到再出现运动
|
wait_motion — 已识别过,菜品静止中忽略,直到菜品再出现运动
|
||||||
"""
|
"""
|
||||||
if frame_bgr is None:
|
if frame_bgr is None:
|
||||||
return False
|
return False
|
||||||
fp = self._frame_fingerprint(frame_bgr)
|
fp = self._motion_fingerprint(frame_bgr)
|
||||||
if fp is None:
|
if fp is None:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -1876,7 +1893,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._motion_settle_fp = None
|
self._motion_settle_fp = None
|
||||||
self._motion_still_since = None
|
self._motion_still_since = None
|
||||||
if sim is not None:
|
if sim is not None:
|
||||||
print(f"[live] 检测到运动 sim={sim:.4f},等待静止后识别")
|
print(f"[live] 菜品区域运动 sim={sim:.4f},等待静止后识别")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if state == "moving":
|
if state == "moving":
|
||||||
|
|
@ -1906,7 +1923,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._motion_settle_fp = None
|
self._motion_settle_fp = None
|
||||||
self._motion_still_since = None
|
self._motion_still_since = None
|
||||||
self._motion_prev_fp = fp
|
self._motion_prev_fp = fp
|
||||||
print(f"[live] 静止满 {hold:g}s,触发 YOLO 识别一次")
|
print(f"[live] 菜品静止满 {hold:g}s,触发 YOLO 识别一次")
|
||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
@ -1918,6 +1935,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._last_reported_sig = None
|
self._last_reported_sig = None
|
||||||
self._last_detect_fp = None
|
self._last_detect_fp = None
|
||||||
self._last_live_dishes = None
|
self._last_live_dishes = None
|
||||||
|
self._last_motion_boxes = None
|
||||||
self._reset_motion_gate(initial_state="moving")
|
self._reset_motion_gate(initial_state="moving")
|
||||||
job = getattr(self, "_live_job", None)
|
job = getattr(self, "_live_job", None)
|
||||||
if job is not None:
|
if job is not None:
|
||||||
|
|
@ -1988,6 +2006,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._live_stable_committed_sig = None
|
self._live_stable_committed_sig = None
|
||||||
self._last_detect_fp = None
|
self._last_detect_fp = None
|
||||||
self._last_live_dishes = None
|
self._last_live_dishes = None
|
||||||
|
self._last_motion_boxes = None
|
||||||
self._reset_motion_gate(initial_state="moving")
|
self._reset_motion_gate(initial_state="moving")
|
||||||
if hasattr(self, "camera_btn"):
|
if hasattr(self, "camera_btn"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -2136,13 +2155,77 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
)
|
)
|
||||||
return Image.alpha_composite(rgba, overlay).convert("RGB")
|
return Image.alpha_composite(rgba, overlay).convert("RGB")
|
||||||
|
|
||||||
def _frame_fingerprint(self, frame_bgr):
|
def _normalize_motion_boxes(self, boxes, width, height):
|
||||||
"""小图灰度指纹,用于静止画面相似度判断(轻量、抗轻微噪声)。"""
|
"""把菜品框规范成像素矩形列表;无效则空列表。"""
|
||||||
|
out = []
|
||||||
|
for box in boxes or []:
|
||||||
|
try:
|
||||||
|
if len(box) < 4:
|
||||||
|
continue
|
||||||
|
x1, y1, x2, y2 = [int(round(float(v))) for v in box[:4]]
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
x1 = max(0, min(width - 1, x1))
|
||||||
|
x2 = max(0, min(width, x2))
|
||||||
|
y1 = max(0, min(height - 1, y1))
|
||||||
|
y2 = max(0, min(height, y2))
|
||||||
|
if x2 - x1 < 8 or y2 - y1 < 8:
|
||||||
|
continue
|
||||||
|
out.append((x1, y1, x2, y2))
|
||||||
|
return out
|
||||||
|
|
||||||
|
def _motion_roi_boxes(self, frame_bgr, boxes=None):
|
||||||
|
"""运动判定 ROI:优先上次/传入餐盘框(略扩张);否则用画面中央放盘区。"""
|
||||||
|
if frame_bgr is None:
|
||||||
|
return []
|
||||||
|
h, w = frame_bgr.shape[:2]
|
||||||
|
rois = self._normalize_motion_boxes(boxes, w, h)
|
||||||
|
if not rois:
|
||||||
|
# 中央约 60% 区域,避开两侧行人
|
||||||
|
mw, mh = int(w * 0.6), int(h * 0.6)
|
||||||
|
x1 = max(0, (w - mw) // 2)
|
||||||
|
y1 = max(0, (h - mh) // 2)
|
||||||
|
return [(x1, y1, x1 + mw, y1 + mh)]
|
||||||
|
pad_x = max(8, int(w * 0.03))
|
||||||
|
pad_y = max(8, int(h * 0.03))
|
||||||
|
expanded = []
|
||||||
|
for x1, y1, x2, y2 in rois:
|
||||||
|
expanded.append((
|
||||||
|
max(0, x1 - pad_x),
|
||||||
|
max(0, y1 - pad_y),
|
||||||
|
min(w, x2 + pad_x),
|
||||||
|
min(h, y2 + pad_y),
|
||||||
|
))
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
def _frame_fingerprint(self, frame_bgr, boxes=None):
|
||||||
|
"""小图灰度指纹,用于静止/运动判断(默认只看餐盘/中央 ROI,抗旁人晃动)。"""
|
||||||
if frame_bgr is None:
|
if frame_bgr is None:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
|
gray = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2GRAY)
|
||||||
small = cv2.resize(gray, (64, 64), interpolation=cv2.INTER_AREA)
|
h, w = gray.shape[:2]
|
||||||
|
rois = self._motion_roi_boxes(frame_bgr, boxes)
|
||||||
|
if len(rois) == 1:
|
||||||
|
x1, y1, x2, y2 = rois[0]
|
||||||
|
region = gray[y1:y2, x1:x2]
|
||||||
|
else:
|
||||||
|
# 多盘:拼成横向条带,避免整帧边缘干扰
|
||||||
|
crops = [gray[y1:y2, x1:x2] for x1, y1, x2, y2 in rois]
|
||||||
|
target_h = 64
|
||||||
|
resized = []
|
||||||
|
for crop in crops:
|
||||||
|
if crop.size == 0:
|
||||||
|
continue
|
||||||
|
ch, cw = crop.shape[:2]
|
||||||
|
nw = max(8, int(round(cw * (target_h / float(ch)))))
|
||||||
|
resized.append(cv2.resize(crop, (nw, target_h), interpolation=cv2.INTER_AREA))
|
||||||
|
if not resized:
|
||||||
|
return None
|
||||||
|
region = np.hstack(resized)
|
||||||
|
if region is None or region.size == 0:
|
||||||
|
return None
|
||||||
|
small = cv2.resize(region, (64, 64), interpolation=cv2.INTER_AREA)
|
||||||
small = cv2.GaussianBlur(small, (3, 3), 0)
|
small = cv2.GaussianBlur(small, (3, 3), 0)
|
||||||
v = small.astype(np.float32).ravel()
|
v = small.astype(np.float32).ravel()
|
||||||
v -= float(v.mean())
|
v -= float(v.mean())
|
||||||
|
|
@ -2271,9 +2354,9 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self.root.after(0, _moving_tip)
|
self.root.after(0, _moving_tip)
|
||||||
return
|
return
|
||||||
|
|
||||||
fp = self._frame_fingerprint(frame)
|
stem = f"_live_frame_{int(time.time() * 1000)}"
|
||||||
stem = Path(save_path).stem if save_path else f"_live_{int(time.time() * 1000)}"
|
|
||||||
# 内存直传 YOLO;裁剪落盘供购物车缩略图 / 待学习;跳过中间 Plate 可视化
|
# 内存直传 YOLO;裁剪落盘供购物车缩略图 / 待学习;跳过中间 Plate 可视化
|
||||||
|
# 整帧 images/ 仅在「有一单」时由 _save_order_image 写入
|
||||||
self.root.after(0, lambda: self._set_result_text("扫描中:YOLO 检测餐盘…", "busy"))
|
self.root.after(0, lambda: self._set_result_text("扫描中:YOLO 检测餐盘…", "busy"))
|
||||||
visualize_save_path, crop_items, boxes = self.app.process_image(
|
visualize_save_path, crop_items, boxes = self.app.process_image(
|
||||||
frame,
|
frame,
|
||||||
|
|
@ -2284,23 +2367,25 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
cropped_file_paths = [c.get("path") for c in crop_items]
|
cropped_file_paths = [c.get("path") for c in crop_items]
|
||||||
crop_arrays = [c.get("image") for c in crop_items]
|
crop_arrays = [c.get("image") for c in crop_items]
|
||||||
|
|
||||||
# 识别完成后进入 wait_motion:持续静止不再识别,须再动→静
|
# 识别完成后进入 wait_motion:只盯餐盘区域,旁人晃动不触发
|
||||||
self._last_detect_fp = fp
|
self._last_motion_boxes = list(boxes or []) if crop_items else None
|
||||||
|
self._last_detect_fp = self._motion_fingerprint(frame)
|
||||||
self._motion_state = "wait_motion"
|
self._motion_state = "wait_motion"
|
||||||
self._yolo_trigger_pending = False
|
self._yolo_trigger_pending = False
|
||||||
|
|
||||||
if not crop_items:
|
if not crop_items:
|
||||||
self._last_live_dishes = []
|
self._last_live_dishes = []
|
||||||
|
self._last_motion_boxes = None
|
||||||
self._reset_live_stability()
|
self._reset_live_stability()
|
||||||
|
|
||||||
def _no_plate():
|
def _no_plate():
|
||||||
self._live_overlay_dishes = []
|
self._live_overlay_dishes = []
|
||||||
self.render_result_dock([], None)
|
# 未检出新菜:左侧已点清单保留上次结果,不清空
|
||||||
self._set_result_text("扫描中:未检测到餐盘", "warn")
|
self._set_result_text("扫描中:未检测到餐盘(已点清单保持上次)", "warn")
|
||||||
if hasattr(self, "scan_tip_label"):
|
if hasattr(self, "scan_tip_label"):
|
||||||
try:
|
try:
|
||||||
self.scan_tip_label.config(
|
self.scan_tip_label.config(
|
||||||
text="扫描中 · 未检出餐盘 · 静止中等待下一次运动"
|
text="扫描中 · 未检出餐盘 · 左侧保持上次已点 · 等待下一次运动"
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
@ -2315,7 +2400,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
final_result, _, unknown_count, labeled_path, dishes = self.run_plate_predict_batch(
|
final_result, _, unknown_count, labeled_path, dishes = self.run_plate_predict_batch(
|
||||||
frame if frame is not None else save_path,
|
frame,
|
||||||
cropped_file_paths,
|
cropped_file_paths,
|
||||||
boxes,
|
boxes,
|
||||||
auto_pending=False,
|
auto_pending=False,
|
||||||
|
|
@ -2323,16 +2408,26 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
det_confs=[c.get("conf") for c in crop_items],
|
det_confs=[c.get("conf") for c in crop_items],
|
||||||
)
|
)
|
||||||
self._last_live_dishes = list(dishes or [])
|
self._last_live_dishes = list(dishes or [])
|
||||||
preview = labeled_path or visualize_save_path or save_path
|
self._last_motion_boxes = [
|
||||||
|
d.get("box") for d in (dishes or []) if d.get("box")
|
||||||
|
] or list(boxes or [])
|
||||||
|
# 用识别后的餐盘框再锚一次,后续 wait_motion 更稳
|
||||||
|
self._last_detect_fp = self._motion_fingerprint(frame)
|
||||||
known_dishes = [d for d in dishes if not d.get("is_unknown")]
|
known_dishes = [d for d in dishes if not d.get("is_unknown")]
|
||||||
# 实时:仅有已识别菜才上报;相同结果只报一次;排餐比对不刷 pending
|
# 有一单(至少 1 个已识别菜,且与上次不同)才存 images/ 并上报;无单不落盘
|
||||||
if known_dishes and save_path:
|
save_path = None
|
||||||
self.report_recognition(
|
if known_dishes:
|
||||||
save_path, known_dishes, preview,
|
sig = self._report_signature(known_dishes)
|
||||||
schedule_check=False, dedupe=True,
|
if sig and sig != self._last_reported_sig:
|
||||||
)
|
save_path = self._save_order_image(frame)
|
||||||
|
if save_path:
|
||||||
|
self.report_recognition(
|
||||||
|
save_path, known_dishes, labeled_path or visualize_save_path or save_path,
|
||||||
|
schedule_check=False, dedupe=True,
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self._last_reported_sig = None
|
self._last_reported_sig = None
|
||||||
|
preview = labeled_path or visualize_save_path or save_path
|
||||||
|
|
||||||
# 同一结果稳定满 hold 秒 → 未知菜写入待学习(同场景一次;盘走不刷入)
|
# 同一结果稳定满 hold 秒 → 未知菜写入待学习(同场景一次;盘走不刷入)
|
||||||
commit_payload = self._update_live_stability(dishes)
|
commit_payload = self._update_live_stability(dishes)
|
||||||
|
|
@ -2376,8 +2471,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
msg += f"|未知 {len(unknown)}(不入清单)"
|
msg += f"|未知 {len(unknown)}(不入清单)"
|
||||||
self._set_result_text(msg, "ok" if not unknown else "warn")
|
self._set_result_text(msg, "ok" if not unknown else "warn")
|
||||||
else:
|
else:
|
||||||
|
keep_hint = "|左侧保持上次已点" if self._cart else ""
|
||||||
self._set_result_text(
|
self._set_result_text(
|
||||||
txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品",
|
(txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品")
|
||||||
|
+ keep_hint,
|
||||||
"warn",
|
"warn",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -2439,7 +2536,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
self._set_result_text("已清空本次已点菜品", "ok")
|
self._set_result_text("已清空本次已点菜品", "ok")
|
||||||
|
|
||||||
def fill_cart_from_dishes(self, dishes):
|
def fill_cart_from_dishes(self, dishes):
|
||||||
"""左侧清单只放已识别菜;未知仅留在预览标注 / 待学习流程。"""
|
"""左侧清单只放已识别菜;未知仅留在预览标注 / 待学习流程。
|
||||||
|
|
||||||
|
若本次没有新的已识别菜,则保留上次清单不刷新。
|
||||||
|
"""
|
||||||
cart = []
|
cart = []
|
||||||
for d in dishes or []:
|
for d in dishes or []:
|
||||||
if d.get("is_unknown"):
|
if d.get("is_unknown"):
|
||||||
|
|
@ -2456,8 +2556,13 @@ class BaseDishApp(PendingLearningUIMixin):
|
||||||
"pending_id": d.get("pending_id"),
|
"pending_id": d.get("pending_id"),
|
||||||
"suggested_category": d.get("suggested_category"),
|
"suggested_category": d.get("suggested_category"),
|
||||||
})
|
})
|
||||||
self._cart = cart
|
if cart:
|
||||||
self._last_dishes = dishes or []
|
self._cart = cart
|
||||||
|
self._last_dishes = dishes or []
|
||||||
|
elif not self._cart:
|
||||||
|
# 本来就是空清单,同步记录本次结果
|
||||||
|
self._last_dishes = dishes or []
|
||||||
|
# else: 本次无新已识别菜 → 左侧继续展示上次已点菜品
|
||||||
|
|
||||||
def _change_qty(self, idx, delta):
|
def _change_qty(self, idx, delta):
|
||||||
if idx < 0 or idx >= len(self._cart):
|
if idx < 0 or idx >= len(self._cart):
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,113 @@ def resolve_in_today_menu(dish_name, match_scope=None):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _period_menu_choices():
|
||||||
|
"""当前餐段菜名列表,供学习下拉。返回 (餐段名, [菜名...], 是否当前餐段)。
|
||||||
|
|
||||||
|
有当前餐段 → 只返回该餐段菜品;
|
||||||
|
不在任何餐段 → 回退今日全部菜品(label=今日菜单)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from dao.meal_schedule import get_meal_schedule_store
|
||||||
|
store = get_meal_schedule_store()
|
||||||
|
period = store.current_period() or {}
|
||||||
|
period_names = sorted(store.current_period_dish_names() or [])
|
||||||
|
if period_names:
|
||||||
|
pname = (period.get("mealPeriodName") or "").strip() or "当前餐段"
|
||||||
|
return pname, period_names, True
|
||||||
|
today_names = sorted(store.today_dish_names() or [])
|
||||||
|
return "今日菜单", today_names, False
|
||||||
|
except Exception:
|
||||||
|
return "当前餐段", [], False
|
||||||
|
|
||||||
|
|
||||||
class PendingLearningUIMixin:
|
class PendingLearningUIMixin:
|
||||||
"""待学习库界面与操作。"""
|
"""待学习库界面与操作。"""
|
||||||
|
|
||||||
# ---------- 通用小工具(不依赖宿主实现) ----------
|
# ---------- 通用小工具(不依赖宿主实现) ----------
|
||||||
|
def _pl_learn_name_choices(self, suggested=""):
|
||||||
|
"""学习下拉候选:仅当前餐段菜品(不在餐段时用今日菜单)。"""
|
||||||
|
period_name, period_names, is_period = _period_menu_choices()
|
||||||
|
values = list(period_names)
|
||||||
|
suggested = (suggested or "").strip()
|
||||||
|
if suggested and suggested in values:
|
||||||
|
initial = suggested
|
||||||
|
else:
|
||||||
|
initial = ""
|
||||||
|
return period_name, period_names, values, initial, is_period
|
||||||
|
|
||||||
|
def _build_today_menu_section(self, parent):
|
||||||
|
"""待学习库顶部:展示今日菜单,高亮当前餐段。"""
|
||||||
|
try:
|
||||||
|
from dao.meal_schedule import get_meal_schedule_store
|
||||||
|
info = get_meal_schedule_store().periods_for_display()
|
||||||
|
except Exception as exc:
|
||||||
|
info = {"error": str(exc), "periods": [], "current_period": None}
|
||||||
|
|
||||||
|
card = RoundCard(parent, title="今日菜单",
|
||||||
|
subtitle="学习时可从当前餐段菜品下拉选择",
|
||||||
|
radius=RADIUS["md"], padding=10)
|
||||||
|
card.pack(fill=tk.X, padx=14, pady=(8, 4))
|
||||||
|
body = card.body
|
||||||
|
|
||||||
|
periods = info.get("periods") or []
|
||||||
|
cur_name = (info.get("current_period") or "").strip()
|
||||||
|
if info.get("error") and not periods:
|
||||||
|
tk.Label(body, text=f"排餐暂不可用:{info.get('error')}",
|
||||||
|
bg=COLORS["surface"], fg=COLORS["warn"],
|
||||||
|
font=FONTS["small"], wraplength=1000, justify=tk.LEFT
|
||||||
|
).pack(anchor=tk.W)
|
||||||
|
return
|
||||||
|
if not periods:
|
||||||
|
tk.Label(body, text="今日暂无排餐数据,学习时仍可手输菜名。",
|
||||||
|
bg=COLORS["surface"], fg=COLORS["muted"],
|
||||||
|
font=FONTS["small"]).pack(anchor=tk.W)
|
||||||
|
return
|
||||||
|
|
||||||
|
date_txt = info.get("date") or ""
|
||||||
|
head = tk.Frame(body, bg=COLORS["surface"])
|
||||||
|
head.pack(fill=tk.X, pady=(0, 6))
|
||||||
|
if date_txt:
|
||||||
|
Chip(head, text=date_txt, tone="mute", bg=COLORS["surface"]).pack(side=tk.LEFT, padx=(0, 6))
|
||||||
|
if cur_name:
|
||||||
|
Chip(head, text=f"当前:{cur_name}", tone="ok", bg=COLORS["surface"]).pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
for p in periods:
|
||||||
|
is_cur = bool(p.get("is_current"))
|
||||||
|
row_bg = COLORS["surface_alt"] if is_cur else COLORS["surface"]
|
||||||
|
row = tk.Frame(body, bg=row_bg,
|
||||||
|
highlightbackground=COLORS["border"] if is_cur else COLORS["surface"],
|
||||||
|
highlightthickness=1 if is_cur else 0)
|
||||||
|
row.pack(fill=tk.X, pady=3)
|
||||||
|
pad = tk.Frame(row, bg=row_bg)
|
||||||
|
pad.pack(fill=tk.X, padx=8, pady=6)
|
||||||
|
|
||||||
|
title = (p.get("name") or "餐段").strip()
|
||||||
|
st = (p.get("startTime") or "").strip()
|
||||||
|
et = (p.get("endTime") or "").strip()
|
||||||
|
time_txt = f"{st}-{et}" if st or et else ""
|
||||||
|
dishes = p.get("dishes") or []
|
||||||
|
names = [d.get("name") for d in dishes if d.get("name")]
|
||||||
|
|
||||||
|
title_row = tk.Frame(pad, bg=row_bg)
|
||||||
|
title_row.pack(fill=tk.X)
|
||||||
|
tk.Label(title_row, text=title, bg=row_bg,
|
||||||
|
fg=COLORS["accent"] if is_cur else COLORS["text"],
|
||||||
|
font=FONTS["ui_bold"]).pack(side=tk.LEFT)
|
||||||
|
if time_txt:
|
||||||
|
tk.Label(title_row, text=time_txt, bg=row_bg, fg=COLORS["muted"],
|
||||||
|
font=FONTS["small"]).pack(side=tk.LEFT, padx=8)
|
||||||
|
Chip(title_row, text=f"{len(names)} 道",
|
||||||
|
tone="ok" if is_cur else "mute", bg=row_bg).pack(side=tk.LEFT)
|
||||||
|
if is_cur:
|
||||||
|
Chip(title_row, text="当前餐段", tone="ok", bg=row_bg).pack(side=tk.LEFT, padx=6)
|
||||||
|
|
||||||
|
menu_txt = "、".join(names) if names else "(本餐段暂无菜品)"
|
||||||
|
tk.Label(pad, text=menu_txt, bg=row_bg,
|
||||||
|
fg=COLORS["text_body"] if names else COLORS["muted"],
|
||||||
|
font=FONTS["small"], wraplength=1000, justify=tk.LEFT,
|
||||||
|
anchor="w").pack(fill=tk.X, pady=(4, 0))
|
||||||
|
|
||||||
def _pl_toast(self, text, tone="info"):
|
def _pl_toast(self, text, tone="info"):
|
||||||
if hasattr(self, "_toast"):
|
if hasattr(self, "_toast"):
|
||||||
try:
|
try:
|
||||||
|
|
@ -253,7 +356,7 @@ class PendingLearningUIMixin:
|
||||||
pass
|
pass
|
||||||
self._pending_dialog_open = True
|
self._pending_dialog_open = True
|
||||||
|
|
||||||
dialog = self._pl_dialog("待学习菜品库", 1120, 720)
|
dialog = self._pl_dialog("待学习菜品库", 1120, 820)
|
||||||
|
|
||||||
def _close_pending():
|
def _close_pending():
|
||||||
self._pending_dialog_open = False
|
self._pending_dialog_open = False
|
||||||
|
|
@ -289,6 +392,8 @@ class PendingLearningUIMixin:
|
||||||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
||||||
).pack(anchor=tk.W, padx=18)
|
).pack(anchor=tk.W, padx=18)
|
||||||
|
|
||||||
|
self._build_today_menu_section(dialog)
|
||||||
|
|
||||||
if items:
|
if items:
|
||||||
search_row = tk.Frame(dialog, bg=COLORS["bg"])
|
search_row = tk.Frame(dialog, bg=COLORS["bg"])
|
||||||
search_row.pack(fill=tk.X, padx=16, pady=(10, 4))
|
search_row.pack(fill=tk.X, padx=16, pady=(10, 4))
|
||||||
|
|
@ -444,11 +549,8 @@ class PendingLearningUIMixin:
|
||||||
command=win.destroy).pack(pady=(8, 12))
|
command=win.destroy).pack(pady=(8, 12))
|
||||||
|
|
||||||
def _learn_pending_from_dialog(self, pending_id, suggested, dialog):
|
def _learn_pending_from_dialog(self, pending_id, suggested, dialog):
|
||||||
"""待学习库:用下拉框选择已有菜名后学习(也可直接手输新菜名)。"""
|
"""待学习库:下拉仅选当前餐段菜品。"""
|
||||||
try:
|
period_name, period_names, values, initial, is_period = self._pl_learn_name_choices(suggested)
|
||||||
existing_names = [name for name, _cnt, _thumb in self.manager.get_template_list()]
|
|
||||||
except Exception:
|
|
||||||
existing_names = []
|
|
||||||
|
|
||||||
pick = tk.Toplevel(dialog)
|
pick = tk.Toplevel(dialog)
|
||||||
pick.title("选择菜品名称")
|
pick.title("选择菜品名称")
|
||||||
|
|
@ -459,23 +561,31 @@ class PendingLearningUIMixin:
|
||||||
|
|
||||||
tk.Label(pick, text="学习到哪个菜名?", bg=COLORS["bg"], fg=COLORS["text"],
|
tk.Label(pick, text="学习到哪个菜名?", bg=COLORS["bg"], fg=COLORS["text"],
|
||||||
font=FONTS["h2"]).pack(padx=20, pady=(18, 4), anchor=tk.W)
|
font=FONTS["h2"]).pack(padx=20, pady=(18, 4), anchor=tk.W)
|
||||||
tk.Label(pick, text="可从已有菜名中选择,也可以直接输入一个新菜名。",
|
if values and is_period:
|
||||||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
tip = f"可从当前餐段「{period_name}」下拉选择,也可手动输入菜名。"
|
||||||
).pack(padx=20, anchor=tk.W)
|
elif values:
|
||||||
|
tip = f"当前不在餐段时间,可从「{period_name}」下拉选择,也可手动输入。"
|
||||||
|
else:
|
||||||
|
tip = "暂无排餐菜品可选,请手动输入菜名。"
|
||||||
|
tk.Label(pick, text=tip, bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
||||||
|
wraplength=340, justify=tk.LEFT).pack(padx=20, anchor=tk.W)
|
||||||
|
|
||||||
|
if values:
|
||||||
|
Chip(pick, text=f"{period_name} · {len(values)} 道",
|
||||||
|
tone="ok", bg=COLORS["bg"]).pack(padx=20, pady=(8, 0), anchor=tk.W)
|
||||||
|
|
||||||
initial = suggested if suggested and suggested in existing_names else ""
|
|
||||||
name_var = tk.StringVar(value=initial)
|
name_var = tk.StringVar(value=initial)
|
||||||
combo = ttk.Combobox(
|
combo = ttk.Combobox(
|
||||||
pick, textvariable=name_var, values=existing_names,
|
pick, textvariable=name_var, values=values,
|
||||||
width=34, style="Large.TEntry",
|
width=34, style="Large.TEntry",
|
||||||
)
|
)
|
||||||
combo.pack(padx=20, pady=10, fill=tk.X)
|
combo.pack(padx=20, pady=10, fill=tk.X)
|
||||||
combo.focus_set()
|
combo.focus_set()
|
||||||
|
|
||||||
if not existing_names:
|
if not values:
|
||||||
tk.Label(pick, text="当前模板库还没有菜名,输入的新名字会新建一个类别。",
|
tk.Label(pick, text="暂无可选菜名,请直接输入新菜名(将新建类别)。",
|
||||||
bg=COLORS["bg"], fg=COLORS["warn"], font=FONTS["caption"],
|
bg=COLORS["bg"], fg=COLORS["warn"], font=FONTS["caption"],
|
||||||
wraplength=280, justify=tk.LEFT).pack(padx=20, anchor=tk.W)
|
wraplength=300, justify=tk.LEFT).pack(padx=20, anchor=tk.W)
|
||||||
|
|
||||||
btns = tk.Frame(pick, bg=COLORS["bg"])
|
btns = tk.Frame(pick, bg=COLORS["bg"])
|
||||||
btns.pack(fill=tk.X, padx=20, pady=(10, 18))
|
btns.pack(fill=tk.X, padx=20, pady=(10, 18))
|
||||||
|
|
@ -483,7 +593,7 @@ class PendingLearningUIMixin:
|
||||||
def _confirm():
|
def _confirm():
|
||||||
category = (name_var.get() or "").strip()
|
category = (name_var.get() or "").strip()
|
||||||
if not category:
|
if not category:
|
||||||
messagebox.showwarning("提示", "请先输入或选择菜品名称", parent=pick)
|
messagebox.showwarning("提示", "请先选择或输入菜品名称", parent=pick)
|
||||||
return
|
return
|
||||||
ok = self.manager.learn_from_pending(pending_id, category)
|
ok = self.manager.learn_from_pending(pending_id, category)
|
||||||
if ok:
|
if ok:
|
||||||
|
|
@ -544,12 +654,9 @@ class PendingLearningUIMixin:
|
||||||
on_finished(False)
|
on_finished(False)
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
period_name, period_names, learn_values, _, is_period = self._pl_learn_name_choices("")
|
||||||
existing_names = [name for name, _cnt, _thumb in self.manager.get_template_list()]
|
|
||||||
except Exception:
|
|
||||||
existing_names = []
|
|
||||||
|
|
||||||
dialog = self._pl_dialog("新菜确认学习", 860, 620)
|
dialog = self._pl_dialog("新菜确认学习", 860, 680)
|
||||||
|
|
||||||
learned_count = {"n": 0}
|
learned_count = {"n": 0}
|
||||||
thumb_refs = []
|
thumb_refs = []
|
||||||
|
|
@ -561,12 +668,22 @@ class PendingLearningUIMixin:
|
||||||
font=FONTS["h1"]).pack(side=tk.LEFT)
|
font=FONTS["h1"]).pack(side=tk.LEFT)
|
||||||
Chip(head, text=f"{len(unknowns)} 个待确认", tone="warn", bg=COLORS["bg"]
|
Chip(head, text=f"{len(unknowns)} 个待确认", tone="warn", bg=COLORS["bg"]
|
||||||
).pack(side=tk.LEFT, padx=10)
|
).pack(side=tk.LEFT, padx=10)
|
||||||
|
if learn_values:
|
||||||
|
Chip(head, text=f"{period_name} {len(learn_values)}道", tone="ok",
|
||||||
|
bg=COLORS["bg"]).pack(side=tk.LEFT)
|
||||||
|
if learn_values and is_period:
|
||||||
|
tip = f"可从当前餐段「{period_name}」下拉选择,也可手输菜名;直接关闭则保留在待学习库。"
|
||||||
|
elif learn_values:
|
||||||
|
tip = f"可从「{period_name}」下拉选择,也可手输菜名;直接关闭则保留在待学习库。"
|
||||||
|
else:
|
||||||
|
tip = "暂无排餐菜品,可手输菜名学习;直接关闭则保留在待学习库。"
|
||||||
tk.Label(
|
tk.Label(
|
||||||
dialog,
|
dialog,
|
||||||
text="识别置信度低于阈值。确认菜名后可立即学习;直接关闭则保留在待学习库,稍后处理。",
|
text=tip,
|
||||||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"], wraplength=780,
|
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"], wraplength=780,
|
||||||
justify=tk.LEFT,
|
justify=tk.LEFT,
|
||||||
).pack(anchor=tk.W, padx=18)
|
).pack(anchor=tk.W, padx=18)
|
||||||
|
self._build_today_menu_section(dialog)
|
||||||
|
|
||||||
card = RoundCard(dialog, radius=RADIUS["lg"], padding=10, expand_body=True)
|
card = RoundCard(dialog, radius=RADIUS["lg"], padding=10, expand_body=True)
|
||||||
card.pack(fill=tk.BOTH, expand=True, padx=14, pady=10)
|
card.pack(fill=tk.BOTH, expand=True, padx=14, pady=10)
|
||||||
|
|
@ -650,9 +767,10 @@ class PendingLearningUIMixin:
|
||||||
name_row.pack(fill=tk.X, pady=(6, 4))
|
name_row.pack(fill=tk.X, pady=(6, 4))
|
||||||
tk.Label(name_row, text="菜名", bg=COLORS["surface_alt"],
|
tk.Label(name_row, text="菜名", bg=COLORS["surface_alt"],
|
||||||
fg=COLORS["text_body"], font=FONTS["ui"]).pack(side=tk.LEFT)
|
fg=COLORS["text_body"], font=FONTS["ui"]).pack(side=tk.LEFT)
|
||||||
name_var = tk.StringVar(value=suggested)
|
row_initial = suggested if suggested in learn_values else (suggested or "")
|
||||||
|
name_var = tk.StringVar(value=row_initial)
|
||||||
ttk.Combobox(
|
ttk.Combobox(
|
||||||
name_row, textvariable=name_var, values=existing_names, width=26,
|
name_row, textvariable=name_var, values=learn_values, width=26,
|
||||||
).pack(side=tk.LEFT, padx=8)
|
).pack(side=tk.LEFT, padx=8)
|
||||||
|
|
||||||
status_label = tk.Label(mid, text="待确认", bg=COLORS["surface_alt"],
|
status_label = tk.Label(mid, text="待确认", bg=COLORS["surface_alt"],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue