更新待学习库,和判断运动更新,
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_snapshot = None # 稳定期间保留的 dishes,供写入
|
||||
self._last_reported_sig = None # 实时上报去重:相同结果只报一次
|
||||
self._last_detect_fp = None # 识别后锚点指纹(静止等待运动)
|
||||
self._last_detect_fp = None # 识别后锚点指纹(静止等待运动;仅餐盘区域)
|
||||
self._last_live_dishes = None # 上一轮识别结果(跳过检测时推进稳定计时)
|
||||
self._last_motion_boxes = None # 上一轮餐盘框,运动判定只看菜品区域
|
||||
self._frame_sim_threshold = 0.992 # 帧相似度阈值:判定静止 / 运动
|
||||
# 运动状态机:动→静保持 still_hold 秒 → YOLO 一次 → 静待运动 → 再静…
|
||||
self._live_still_hold_seconds = float(DISH_LIVE_STILL_HOLD_SECONDS)
|
||||
|
|
@ -194,9 +195,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
return None
|
||||
|
||||
def take_live_frame(self):
|
||||
"""抓取实时帧;返回 (0, path, frame) 或 (-1, None, None)。
|
||||
"""抓取实时帧(默认不落盘)。返回 (0, None, frame) 或 (-1, None, None)。
|
||||
|
||||
优先复用预览缓存帧,避免与预览争抢相机;磁盘路径供上报/待学习。
|
||||
优先复用预览缓存帧,避免与预览争抢相机。
|
||||
有订单时再调用 _save_order_image 写入 images/。
|
||||
"""
|
||||
frame = None
|
||||
with self._live_lock:
|
||||
|
|
@ -209,19 +211,25 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
frame = self.grab_preview_frame()
|
||||
if frame is 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)
|
||||
stamp = int(time.time() * 1000)
|
||||
save_path = os.path.abspath(os.path.join("images", f"_live_frame_{stamp}.jpg"))
|
||||
try:
|
||||
if cv2.imwrite(save_path, frame):
|
||||
self._cleanup_old_live_frames(keep=30)
|
||||
return 0, save_path, frame
|
||||
if cv2.imwrite(save_path, frame_bgr):
|
||||
self._cleanup_old_live_frames(keep=3000000)
|
||||
return save_path
|
||||
except Exception as e:
|
||||
print(f"保存实时帧失败: {e}")
|
||||
return -1, None, None
|
||||
print(f"保存订单帧失败: {e}")
|
||||
return None
|
||||
|
||||
def _cleanup_old_live_frames(self, keep=30):
|
||||
"""清理过期实时帧,防止 images/ 无限增长。"""
|
||||
"""清理过期订单帧,防止 images/ 无限增长。"""
|
||||
try:
|
||||
folder = os.path.abspath("images")
|
||||
files = []
|
||||
|
|
@ -1850,17 +1858,26 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._motion_still_since = None
|
||||
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):
|
||||
"""根据当前帧更新运动状态;从动到静保持 hold 秒时置位 YOLO 触发。
|
||||
|
||||
运动只看餐盘区域(有上次检测框用框,否则画面中央),避免旁人晃动误触发。
|
||||
|
||||
状态:
|
||||
moving — 画面在动(或刚开扫)
|
||||
settling — 刚停住,累计静止时间
|
||||
wait_motion — 已识别过,静止中忽略,直到再出现运动
|
||||
moving — 菜品区域在动(或刚开扫)
|
||||
settling — 菜品刚停住,累计静止时间
|
||||
wait_motion — 已识别过,菜品静止中忽略,直到菜品再出现运动
|
||||
"""
|
||||
if frame_bgr is None:
|
||||
return False
|
||||
fp = self._frame_fingerprint(frame_bgr)
|
||||
fp = self._motion_fingerprint(frame_bgr)
|
||||
if fp is None:
|
||||
return False
|
||||
|
||||
|
|
@ -1876,7 +1893,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._motion_settle_fp = None
|
||||
self._motion_still_since = None
|
||||
if sim is not None:
|
||||
print(f"[live] 检测到运动 sim={sim:.4f},等待静止后识别")
|
||||
print(f"[live] 菜品区域运动 sim={sim:.4f},等待静止后识别")
|
||||
return False
|
||||
|
||||
if state == "moving":
|
||||
|
|
@ -1906,7 +1923,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._motion_settle_fp = None
|
||||
self._motion_still_since = None
|
||||
self._motion_prev_fp = fp
|
||||
print(f"[live] 静止满 {hold:g}s,触发 YOLO 识别一次")
|
||||
print(f"[live] 菜品静止满 {hold:g}s,触发 YOLO 识别一次")
|
||||
return True
|
||||
return False
|
||||
|
||||
|
|
@ -1918,6 +1935,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._last_reported_sig = None
|
||||
self._last_detect_fp = None
|
||||
self._last_live_dishes = None
|
||||
self._last_motion_boxes = None
|
||||
self._reset_motion_gate(initial_state="moving")
|
||||
job = getattr(self, "_live_job", None)
|
||||
if job is not None:
|
||||
|
|
@ -1988,6 +2006,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._live_stable_committed_sig = None
|
||||
self._last_detect_fp = None
|
||||
self._last_live_dishes = None
|
||||
self._last_motion_boxes = None
|
||||
self._reset_motion_gate(initial_state="moving")
|
||||
if hasattr(self, "camera_btn"):
|
||||
try:
|
||||
|
|
@ -2136,13 +2155,77 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
)
|
||||
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:
|
||||
return None
|
||||
try:
|
||||
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)
|
||||
v = small.astype(np.float32).ravel()
|
||||
v -= float(v.mean())
|
||||
|
|
@ -2271,9 +2354,9 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self.root.after(0, _moving_tip)
|
||||
return
|
||||
|
||||
fp = self._frame_fingerprint(frame)
|
||||
stem = Path(save_path).stem if save_path else f"_live_{int(time.time() * 1000)}"
|
||||
stem = f"_live_frame_{int(time.time() * 1000)}"
|
||||
# 内存直传 YOLO;裁剪落盘供购物车缩略图 / 待学习;跳过中间 Plate 可视化
|
||||
# 整帧 images/ 仅在「有一单」时由 _save_order_image 写入
|
||||
self.root.after(0, lambda: self._set_result_text("扫描中:YOLO 检测餐盘…", "busy"))
|
||||
visualize_save_path, crop_items, boxes = self.app.process_image(
|
||||
frame,
|
||||
|
|
@ -2284,23 +2367,25 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
cropped_file_paths = [c.get("path") for c in crop_items]
|
||||
crop_arrays = [c.get("image") for c in crop_items]
|
||||
|
||||
# 识别完成后进入 wait_motion:持续静止不再识别,须再动→静
|
||||
self._last_detect_fp = fp
|
||||
# 识别完成后进入 wait_motion:只盯餐盘区域,旁人晃动不触发
|
||||
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._yolo_trigger_pending = False
|
||||
|
||||
if not crop_items:
|
||||
self._last_live_dishes = []
|
||||
self._last_motion_boxes = None
|
||||
self._reset_live_stability()
|
||||
|
||||
def _no_plate():
|
||||
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"):
|
||||
try:
|
||||
self.scan_tip_label.config(
|
||||
text="扫描中 · 未检出餐盘 · 静止中等待下一次运动"
|
||||
text="扫描中 · 未检出餐盘 · 左侧保持上次已点 · 等待下一次运动"
|
||||
)
|
||||
except Exception:
|
||||
pass
|
||||
|
|
@ -2315,7 +2400,7 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
),
|
||||
)
|
||||
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,
|
||||
boxes,
|
||||
auto_pending=False,
|
||||
|
|
@ -2323,16 +2408,26 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
det_confs=[c.get("conf") for c in crop_items],
|
||||
)
|
||||
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")]
|
||||
# 实时:仅有已识别菜才上报;相同结果只报一次;排餐比对不刷 pending
|
||||
if known_dishes and save_path:
|
||||
self.report_recognition(
|
||||
save_path, known_dishes, preview,
|
||||
schedule_check=False, dedupe=True,
|
||||
)
|
||||
# 有一单(至少 1 个已识别菜,且与上次不同)才存 images/ 并上报;无单不落盘
|
||||
save_path = None
|
||||
if known_dishes:
|
||||
sig = self._report_signature(known_dishes)
|
||||
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:
|
||||
self._last_reported_sig = None
|
||||
preview = labeled_path or visualize_save_path or save_path
|
||||
|
||||
# 同一结果稳定满 hold 秒 → 未知菜写入待学习(同场景一次;盘走不刷入)
|
||||
commit_payload = self._update_live_stability(dishes)
|
||||
|
|
@ -2376,8 +2471,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
msg += f"|未知 {len(unknown)}(不入清单)"
|
||||
self._set_result_text(msg, "ok" if not unknown else "warn")
|
||||
else:
|
||||
keep_hint = "|左侧保持上次已点" if self._cart else ""
|
||||
self._set_result_text(
|
||||
txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品",
|
||||
(txt or f"扫描中|检出 {len(ds)} 盘,尚无达到阈值的菜品")
|
||||
+ keep_hint,
|
||||
"warn",
|
||||
)
|
||||
|
||||
|
|
@ -2439,7 +2536,10 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
self._set_result_text("已清空本次已点菜品", "ok")
|
||||
|
||||
def fill_cart_from_dishes(self, dishes):
|
||||
"""左侧清单只放已识别菜;未知仅留在预览标注 / 待学习流程。"""
|
||||
"""左侧清单只放已识别菜;未知仅留在预览标注 / 待学习流程。
|
||||
|
||||
若本次没有新的已识别菜,则保留上次清单不刷新。
|
||||
"""
|
||||
cart = []
|
||||
for d in dishes or []:
|
||||
if d.get("is_unknown"):
|
||||
|
|
@ -2456,8 +2556,13 @@ class BaseDishApp(PendingLearningUIMixin):
|
|||
"pending_id": d.get("pending_id"),
|
||||
"suggested_category": d.get("suggested_category"),
|
||||
})
|
||||
self._cart = cart
|
||||
self._last_dishes = dishes or []
|
||||
if cart:
|
||||
self._cart = cart
|
||||
self._last_dishes = dishes or []
|
||||
elif not self._cart:
|
||||
# 本来就是空清单,同步记录本次结果
|
||||
self._last_dishes = dishes or []
|
||||
# else: 本次无新已识别菜 → 左侧继续展示上次已点菜品
|
||||
|
||||
def _change_qty(self, idx, delta):
|
||||
if idx < 0 or idx >= len(self._cart):
|
||||
|
|
|
|||
|
|
@ -117,10 +117,113 @@ def resolve_in_today_menu(dish_name, match_scope=None):
|
|||
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:
|
||||
"""待学习库界面与操作。"""
|
||||
|
||||
# ---------- 通用小工具(不依赖宿主实现) ----------
|
||||
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"):
|
||||
if hasattr(self, "_toast"):
|
||||
try:
|
||||
|
|
@ -253,7 +356,7 @@ class PendingLearningUIMixin:
|
|||
pass
|
||||
self._pending_dialog_open = True
|
||||
|
||||
dialog = self._pl_dialog("待学习菜品库", 1120, 720)
|
||||
dialog = self._pl_dialog("待学习菜品库", 1120, 820)
|
||||
|
||||
def _close_pending():
|
||||
self._pending_dialog_open = False
|
||||
|
|
@ -289,6 +392,8 @@ class PendingLearningUIMixin:
|
|||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
||||
).pack(anchor=tk.W, padx=18)
|
||||
|
||||
self._build_today_menu_section(dialog)
|
||||
|
||||
if items:
|
||||
search_row = tk.Frame(dialog, bg=COLORS["bg"])
|
||||
search_row.pack(fill=tk.X, padx=16, pady=(10, 4))
|
||||
|
|
@ -444,11 +549,8 @@ class PendingLearningUIMixin:
|
|||
command=win.destroy).pack(pady=(8, 12))
|
||||
|
||||
def _learn_pending_from_dialog(self, pending_id, suggested, dialog):
|
||||
"""待学习库:用下拉框选择已有菜名后学习(也可直接手输新菜名)。"""
|
||||
try:
|
||||
existing_names = [name for name, _cnt, _thumb in self.manager.get_template_list()]
|
||||
except Exception:
|
||||
existing_names = []
|
||||
"""待学习库:下拉仅选当前餐段菜品。"""
|
||||
period_name, period_names, values, initial, is_period = self._pl_learn_name_choices(suggested)
|
||||
|
||||
pick = tk.Toplevel(dialog)
|
||||
pick.title("选择菜品名称")
|
||||
|
|
@ -459,23 +561,31 @@ class PendingLearningUIMixin:
|
|||
|
||||
tk.Label(pick, text="学习到哪个菜名?", bg=COLORS["bg"], fg=COLORS["text"],
|
||||
font=FONTS["h2"]).pack(padx=20, pady=(18, 4), anchor=tk.W)
|
||||
tk.Label(pick, text="可从已有菜名中选择,也可以直接输入一个新菜名。",
|
||||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"],
|
||||
).pack(padx=20, anchor=tk.W)
|
||||
if values and is_period:
|
||||
tip = f"可从当前餐段「{period_name}」下拉选择,也可手动输入菜名。"
|
||||
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)
|
||||
combo = ttk.Combobox(
|
||||
pick, textvariable=name_var, values=existing_names,
|
||||
pick, textvariable=name_var, values=values,
|
||||
width=34, style="Large.TEntry",
|
||||
)
|
||||
combo.pack(padx=20, pady=10, fill=tk.X)
|
||||
combo.focus_set()
|
||||
|
||||
if not existing_names:
|
||||
tk.Label(pick, text="当前模板库还没有菜名,输入的新名字会新建一个类别。",
|
||||
if not values:
|
||||
tk.Label(pick, text="暂无可选菜名,请直接输入新菜名(将新建类别)。",
|
||||
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.pack(fill=tk.X, padx=20, pady=(10, 18))
|
||||
|
|
@ -483,7 +593,7 @@ class PendingLearningUIMixin:
|
|||
def _confirm():
|
||||
category = (name_var.get() or "").strip()
|
||||
if not category:
|
||||
messagebox.showwarning("提示", "请先输入或选择菜品名称", parent=pick)
|
||||
messagebox.showwarning("提示", "请先选择或输入菜品名称", parent=pick)
|
||||
return
|
||||
ok = self.manager.learn_from_pending(pending_id, category)
|
||||
if ok:
|
||||
|
|
@ -544,12 +654,9 @@ class PendingLearningUIMixin:
|
|||
on_finished(False)
|
||||
return
|
||||
|
||||
try:
|
||||
existing_names = [name for name, _cnt, _thumb in self.manager.get_template_list()]
|
||||
except Exception:
|
||||
existing_names = []
|
||||
period_name, period_names, learn_values, _, is_period = self._pl_learn_name_choices("")
|
||||
|
||||
dialog = self._pl_dialog("新菜确认学习", 860, 620)
|
||||
dialog = self._pl_dialog("新菜确认学习", 860, 680)
|
||||
|
||||
learned_count = {"n": 0}
|
||||
thumb_refs = []
|
||||
|
|
@ -561,12 +668,22 @@ class PendingLearningUIMixin:
|
|||
font=FONTS["h1"]).pack(side=tk.LEFT)
|
||||
Chip(head, text=f"{len(unknowns)} 个待确认", tone="warn", bg=COLORS["bg"]
|
||||
).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(
|
||||
dialog,
|
||||
text="识别置信度低于阈值。确认菜名后可立即学习;直接关闭则保留在待学习库,稍后处理。",
|
||||
text=tip,
|
||||
bg=COLORS["bg"], fg=COLORS["muted"], font=FONTS["small"], wraplength=780,
|
||||
justify=tk.LEFT,
|
||||
).pack(anchor=tk.W, padx=18)
|
||||
self._build_today_menu_section(dialog)
|
||||
|
||||
card = RoundCard(dialog, radius=RADIUS["lg"], padding=10, expand_body=True)
|
||||
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))
|
||||
tk.Label(name_row, text="菜名", bg=COLORS["surface_alt"],
|
||||
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(
|
||||
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)
|
||||
|
||||
status_label = tk.Label(mid, text="待确认", bg=COLORS["surface_alt"],
|
||||
|
|
|
|||
Loading…
Reference in New Issue