# coding=utf-8
import os
import time
CAIHUI_DATABASE = {
'name': os.environ.get('DISH_DB_NAME', 'goods'),
'host': os.environ.get('CARC_CRAWLER_DB_HOST', '127.0.0.1'),
'port': int(os.environ.get('CARC_CRAWLER_DB_PORT', 3306)),
'user': os.environ.get('CARC_CRAWLER_DB_USER', 'root'),
'password': os.environ.get('CARC_CRAWLER_DB_PASSWORD', '123456'),
}
# SQLite 本地临时缓存(识别读此库;变更后从 MySQL 拉取刷新)
DISH_SQLITE_PATH = os.environ.get(
'DISH_SQLITE_PATH',
os.path.join(os.path.dirname(__file__), 'dish_templates.db'),
)
# 本机基地编码(学菜写入 source_base_id;结果 API 的 base_id 默认也用它)
# 建议填管理系统返回的 code,例如「上海路基地-1788059884828-e55vu」
DISH_BASE_CODE = os.environ.get('DISH_BASE_CODE', 'BASE_LOCAL_01')
DISH_BASE_NAME = os.environ.get('DISH_BASE_NAME', '奥体基地')
DISH_BASE_REGION = os.environ.get('DISH_BASE_REGION', '')
# 管理系统:公开接口(X-API-Key)
# 完整 Key 见 docs/第三方接口文档_mgmt.md(含 dish_ 前缀)
DISH_MGMT_API_ORIGIN = os.environ.get(
'DISH_MGMT_API_ORIGIN',
'https://cpsb.ruimeida.cc',
).rstrip('/')
DISH_MGMT_API_URL = os.environ.get(
'DISH_MGMT_API_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/bases',
)
DISH_MGMT_RESULT_API_URL = os.environ.get(
'DISH_MGMT_RESULT_API_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/recognition-orders',
)
DISH_MGMT_UNRECOGNIZED_API_URL = os.environ.get(
'DISH_MGMT_UNRECOGNIZED_API_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/unrecognized-samples',
)
DISH_MGMT_MEAL_API_URL = os.environ.get(
'DISH_MGMT_MEAL_API_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/meal-schedules/today',
)
DISH_MGMT_DEVICE_STATUS_URL = os.environ.get(
'DISH_MGMT_DEVICE_STATUS_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/devices/status',
)
DISH_MGMT_DISHES_API_URL = os.environ.get(
'DISH_MGMT_DISHES_API_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/dishes',
)
DISH_MGMT_DISH_LEARNING_STATUS_URL = os.environ.get(
'DISH_MGMT_DISH_LEARNING_STATUS_URL',
f'{DISH_MGMT_API_ORIGIN}/api/public/dishes/learning-status',
)
DISH_MGMT_API_KEY = os.environ.get(
'DISH_MGMT_API_KEY',
'dish_a4f7a1287198179fdc6a6571aa14064c2afc6338c6730e2b668ac5be15bdf769',
)
DISH_MGMT_API_TIMEOUT = float(os.environ.get('DISH_MGMT_API_TIMEOUT', '10'))
DISH_MGMT_SYNC_ON_START = os.environ.get('DISH_MGMT_SYNC_ON_START', '1') == '1'
DISH_MGMT_MEAL_ON_START = os.environ.get('DISH_MGMT_MEAL_ON_START', '1') == '1'
# 识别订单上报失败落盘重发(网络抖动/DNS 失败时不丢单)
DISH_RESULT_RETRY_ENABLED = os.environ.get('DISH_RESULT_RETRY_ENABLED', '1') == '1'
DISH_RESULT_RETRY_DIR = os.environ.get(
'DISH_RESULT_RETRY_DIR',
os.path.join(os.path.dirname(os.path.dirname(__file__)), 'data', 'result_outbox'),
)
DISH_RESULT_RETRY_INTERVAL_SECONDS = float(os.environ.get('DISH_RESULT_RETRY_INTERVAL_SECONDS', '20'))
# 单条最大重试次数;0=一直重试直到成功
DISH_RESULT_RETRY_MAX_ATTEMPTS = int(os.environ.get('DISH_RESULT_RETRY_MAX_ATTEMPTS', '0'))
DISH_RESULT_RETRY_BATCH = int(os.environ.get('DISH_RESULT_RETRY_BATCH', '10'))
# 本机设备(上报设备状态)
DISH_DEVICE_CODE = os.environ.get('DISH_DEVICE_CODE', '') or os.environ.get('DISH_BASE_CODE', DISH_BASE_CODE)
DISH_DEVICE_NAME = os.environ.get('DISH_DEVICE_NAME', '') or os.environ.get('DISH_BASE_NAME', DISH_BASE_NAME)
DISH_DEVICE_STATUS_INTERVAL = int(os.environ.get('DISH_DEVICE_STATUS_INTERVAL', '60')) # 秒
DISH_DEVICE_STATUS_ENABLED = os.environ.get('DISH_DEVICE_STATUS_ENABLED', '1') == '1'
# 启动时是否从 MySQL 拉取;后台定时同步间隔(秒,含上传队列 flush + 拉取)
DISH_SYNC_ON_START = os.environ.get('DISH_SYNC_ON_START', '1') == '1'
DISH_SYNC_INTERVAL = int(os.environ.get('DISH_SYNC_INTERVAL', 300)) # 默认 5 分钟
# 识别框是否显示「似xx 检xx」(默认关;需要时 set DISH_SHOW_BOX_SCORES=1)
DISH_SHOW_BOX_SCORES = os.environ.get('DISH_SHOW_BOX_SCORES', '0') == '1'
# 当前餐段匹配:有当日排餐时仅在「今日当前餐段」菜品中比对,未命中直接未知(不回退大库)
# 1) 最高分 ≥ 0.80 → 认定;2) 未达 0.80 但最高分 > 0.75 → 仍认定;否则未知
DISH_MEAL_SMALL_LIB_THRESHOLD = float(os.environ.get('DISH_MEAL_SMALL_LIB_THRESHOLD', '0.82'))
DISH_MEAL_SMALL_LIB_SOFT_THRESHOLD = float(os.environ.get('DISH_MEAL_SMALL_LIB_SOFT_THRESHOLD', '0.82'))
# 早于该时刻且尚无当日排餐 → 不做当前餐段限制,直接走大库
DISH_MEAL_CUTOFF_HOUR = int(os.environ.get('DISH_MEAL_CUTOFF_HOUR', '7'))
# 主食小库:不在每日排餐中,常驻候选;与「当前餐段菜品小库」双路匹配
# 环境变量 DISH_STAPLE_NAMES 逗号分隔覆盖;仅当本地模板库已有同名模板时才参与比对
_DISH_STAPLE_NAMES_DEFAULT = (
'米饭,白米饭,杂粮饭,黑米饭,馒头,花卷,包子,面条,汤面,炒饭,盖浇饭,粥,白粥,小米粥,馄饨,饺子,'
'酸奶,香蕉,苹果,梨子,葡萄,营养汤,土豆,黄瓜,山药,红薯,玉米,芋头'
)
DISH_STAPLE_NAMES = [
x.strip() for x in os.environ.get('DISH_STAPLE_NAMES', _DISH_STAPLE_NAMES_DEFAULT).split(',')
if x.strip()
]
DISH_STAPLE_LIB_THRESHOLD = float(os.environ.get('DISH_STAPLE_LIB_THRESHOLD', '0.80'))
DISH_STAPLE_LIB_SOFT_THRESHOLD = float(os.environ.get('DISH_STAPLE_LIB_SOFT_THRESHOLD', '0.81'))
# 主食与餐段菜都达标时:主食分 ≥ 菜分 - margin 则优先认主食(减轻主食被菜品抢走)
DISH_STAPLE_PRIORITY_MARGIN = float(os.environ.get('DISH_STAPLE_PRIORITY_MARGIN', '0.03'))
# 每餐开始前多少分钟拉取排餐(默认 60 = 开餐前 1 小时)
DISH_MEAL_PREFETCH_MINUTES = int(os.environ.get('DISH_MEAL_PREFETCH_MINUTES', '60'))
# 尚无餐段信息时的兜底拉取时刻(启动后第一次排餐未到位时用)
DISH_MEAL_REFRESH_HOUR = int(os.environ.get('DISH_MEAL_REFRESH_HOUR', '6'))
DISH_MEAL_REFRESH_MINUTE = int(os.environ.get('DISH_MEAL_REFRESH_MINUTE', '0'))
# 实时扫描:未知菜写入待学习前的最短稳定秒数(客人放盘短,默认 1 秒)
# 盘子移走时不刷入,避免移盘过程中的误检未知
DISH_LIVE_PENDING_STABLE_SECONDS = float(os.environ.get('DISH_LIVE_PENDING_STABLE_SECONDS', '1.0'))
# 运动判定:从动到静后,连续多少帧与静止锚点相似,才触发 YOLO 一次(按相机帧,不按秒)。
# 持续静止不再识别,须再经历「静→动→静」。默认 4 帧(约对应原 0.2s@20Hz 轮询)。
DISH_LIVE_STILL_HOLD_FRAMES = int(os.environ.get('DISH_LIVE_STILL_HOLD_FRAMES', '4'))
# 已有订单时,菜品区域相似度阈值(更低=更宽松,手在中间晃仍算静止,减少误触发)
DISH_LIVE_MOTION_SIM_WITH_ORDER = float(os.environ.get('DISH_LIVE_MOTION_SIM_WITH_ORDER', '0.970'))
# 订单防抖:菜品名称集合去重 + 时间间隔(秒),避免同一次晃动重复上传
# 默认 2s(建议 2~3s)
DISH_LIVE_ORDER_DEBOUNCE_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_DEBOUNCE_SECONDS', '2'))
# 无餐盘持续该秒数后清场解锁,允许下一盘(含相同菜名)重新存图/上报
DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS = float(os.environ.get('DISH_LIVE_ORDER_CLEAR_HOLD_SECONDS', '1.5'))
# 同场景去重:按预览帧计数(约 25fps,默认 60 帧 ≈ 2.4s)。
# 窗口内即使个别已知菜名不同,只要检测框布局相近也视为同一单(不重复上报)
DISH_LIVE_ORDER_SAME_SCENE_FRAMES = int(os.environ.get('DISH_LIVE_ORDER_SAME_SCENE_FRAMES', '60'))
# 框匹配 IoU 阈值;匹配对数占比阈值(0~1)
DISH_LIVE_ORDER_LAYOUT_IOU = float(os.environ.get('DISH_LIVE_ORDER_LAYOUT_IOU', '0.40'))
DISH_LIVE_ORDER_LAYOUT_MATCH_RATIO = float(os.environ.get('DISH_LIVE_ORDER_LAYOUT_MATCH_RATIO', '0.70'))
# 模板库比对后需丢弃的菜名(YOLO 无此类时仍生效):命中「手」「空盘」等则整框过滤,不入清单/不上报
# 环境变量名沿用 DISH_DETECT_FILTER_NAMES
DISH_DETECT_FILTER_NAMES = [
x.strip() for x in os.environ.get('DISH_DETECT_FILTER_NAMES', '手,空盘').split(',')
if x.strip()
]
DISH_MATCH_FILTER_NAMES = DISH_DETECT_FILTER_NAMES
# 贴边不完整框过滤:托盘刚入画只露一角时 YOLO 常会检出,直接丢弃
# 框任一边缘距画面边界 ≤ margin 像素,视为贴边
DISH_DETECT_EDGE_MARGIN_PX = int(os.environ.get('DISH_DETECT_EDGE_MARGIN_PX', '8'))
# 贴边且框面积占整图比例低于此值 → 丢弃(默认 2%)
DISH_DETECT_EDGE_MIN_AREA_RATIO = float(os.environ.get('DISH_DETECT_EDGE_MIN_AREA_RATIO', '0.02'))
# 贴边且框较短边像素低于此值 → 丢弃
DISH_DETECT_EDGE_MIN_SIDE_PX = int(os.environ.get('DISH_DETECT_EDGE_MIN_SIDE_PX', '48'))
# 任意框(不论是否贴边)面积占比低于此值也丢弃;0=关闭
DISH_DETECT_MIN_AREA_RATIO = float(os.environ.get('DISH_DETECT_MIN_AREA_RATIO', '0.008'))
# 用本地视频文件夹代替相机实时流(调试/回放,主要给海康入口用)。
# 1=读 DISH_VIDEO_SOURCE_DIR,0=读相机。usb_camera_app 固定读 USB 相机,不受此项影响。
DISH_USE_VIDEO_FOLDER = os.environ.get('DISH_USE_VIDEO_FOLDER', '0') == '1'
DISH_VIDEO_SOURCE_DIR = os.environ.get('DISH_VIDEO_SOURCE_DIR', 'videos')
# 相机预览流本地录像:相机连接后自动录,只保留最近 N 分钟(滚动删除旧段)
# 视频回放模式下默认关闭,避免边读边写同一目录
_STREAM_RECORD_DEFAULT = '0' if DISH_USE_VIDEO_FOLDER else '1'
DISH_STREAM_RECORD_ENABLED = os.environ.get(
'DISH_STREAM_RECORD_ENABLED', _STREAM_RECORD_DEFAULT,
) == '1'
DISH_STREAM_RECORD_DIR = os.environ.get('DISH_STREAM_RECORD_DIR', 'videos')
DISH_STREAM_RETENTION_MINUTES = float(os.environ.get('DISH_STREAM_RETENTION_MINUTES', '3000'))
# 单段时长(秒),默认 5 分钟一切,便于清理
DISH_STREAM_SEGMENT_SECONDS = float(os.environ.get('DISH_STREAM_SEGMENT_SECONDS', '3600'))
DISH_STREAM_RECORD_FPS = float(os.environ.get('DISH_STREAM_RECORD_FPS', '10'))
DISH_STREAM_MAX_WIDTH = int(os.environ.get('DISH_STREAM_MAX_WIDTH', '1280'))
DEBUG = True
LOG_DIR = os.path.join(os.getcwd(), f'log/{time.strftime("%Y-%m-%d")}.log')
LOG_FORMAT = '{level: <8} {time:YYYY-MM-DD HH:mm:ss.SSS} - {name}:{function} - {message}'