product-recognition-demo/recognization/run-demo-server.sh
tngo aaed5d388f feat(identity): local-feature matcher plus text lane as the default backend
Replaces the DINO gallery as the recognition path with the measured design
from docs/research/2026-09-28-recognition-architecture/DECISION.md: RootSIFT
over a per-SKU reference pyramid, similarity-constrained RANSAC and a hue
check, fused with an OCR text lane that vetoes a match whose package text
names a different catalog row and answers on its own when geometry finds
nothing.

- identity_lanes.py: LocalLane, TextLane, DecisionPolicy. The text lane
  builds each row's signature from the catalog name plus its packshot OCR, so
  a row with no enrolled image is still answerable.
- engine.py: every backend groups scores by canonical SKU, so no
  view-suffixed id reaches the wire and a name and price always resolve.
  decide_crop fuses the lanes; score_crop_lanes feeds the live path. Both
  lanes are built in warm_up (non-fatally) so the first frame is not the slow
  one, and the text lane shares the engine's OCR lock.
- live.py: decides per crop on the lane's own rank order. The lane's order
  comes from its rank key and its value is only the gate, so re-sorting by
  value named a different SKU than the lane chose; averaging in an empty
  observation also diluted a good confidence below the accept line.
- identity.py: a `local` score scale whose gate is one absolute line, because
  the value is already a margin over the runner-up.
- app.py, run-demo-server.sh: `--identity local` is the default; the demo
  server no longer starts on the leaked `stock-lopia` catalog.
- eval_live.py: the replay vocabulary accepts `local`, so a recording from
  the shipped server can be scored.
- README.md: documents the three backends and what each is for.

Measured on the corrected image6 labels at live resolution: 36/61 in-catalog
facings named at 95% precision, 0 of 48 unknowns accepted, 11 labels sourced
from the text lane alone. Through the real server: /v1/recognize 90%
precision, /v1/stream 100% precision, no view-suffixed ids on either.
2026-09-28 22:27:21 +09:00

71 lines
1.9 KiB
Bash
Executable file

#!/usr/bin/env bash
# Start the recognition server and write ignored local app endpoints.
#
# ./run-demo-server.sh
# ./run-demo-server.sh stock-milk150
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
cd "$(dirname "$0")"
STOCK="${1:-stock-lopia-clean}"
PORT="${PORT:-8099}"
TOKEN_FILE=".demo-token"
if [ ! -d "$STOCK" ]; then
echo "no such store: $STOCK" >&2
exit 1
fi
IP="$(ipconfig getifaddr en0 2>/dev/null || ipconfig getifaddr en1 2>/dev/null || true)"
if [ -z "$IP" ]; then
echo "this Mac has no LAN address; the phone cannot reach it" >&2
exit 1
fi
if [ ! -s "$TOKEN_FILE" ]; then
umask 077
openssl rand -hex 32 > "$TOKEN_FILE"
fi
TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")"
umask 077
cat > "$ROOT/iosApp/Configuration/Config.local.xcconfig" <<EOF
APP_RECOGNITION_BASE_URL = http:/\$()/$IP:$PORT
APP_RECOGNITION_API_TOKEN = $TOKEN
EOF
mkdir -p config
cat > config/local.env <<EOF
DORAYAKI_API_TOKEN=$TOKEN
EOF
PROPERTIES="$ROOT/androidApp/local.properties"
touch "$PROPERTIES"
TMP="$(mktemp)"
awk -v url="app.recognitionBaseUrl=http://$IP:$PORT" -v token="app.recognitionApiToken=$TOKEN" '
BEGIN { url_found = 0; token_found = 0 }
$0 ~ /^app\.recognitionBaseUrl=/ { print url; url_found = 1; next }
$0 ~ /^app\.recognitionApiToken=/ { print token; token_found = 1; next }
{ print }
END {
if (!url_found) print url
if (!token_found) print token
}
' "$PROPERTIES" > "$TMP"
mv "$TMP" "$PROPERTIES"
cat <<EOF
Dorayaki demo server
store $STOCK
listening http://0.0.0.0:$PORT
phone URL http://$IP:$PORT
Local app configuration was updated. Rebuild both apps.
The API token is in $TOKEN_FILE and is not printed.
EOF
exec env YOLO_OFFLINE=1 DORAYAKI_STOCK_DIR="$STOCK" DORAYAKI_API_TOKEN="$TOKEN" \
.venv/bin/python -u code/app.py \
--port "$PORT" --host 0.0.0.0 --identity local \
--live-conf 0.15 --classify-workers 8