#!/usr/bin/env bash # Sanad Package 3 (Recognition) entrypoint. # 1) license gate 2) resolve P2 env (env > license > config) 3) preflight 4) launch. set -u PKG="P3" CFG="/app/pkg3_config/p3_config.json" # ── 1. license gate ────────────────────────────────────────────────────────── # license_check exits 0 only when entitled. If NOT entitled we exit the CONTAINER # cleanly (code 0) so a restart policy won't crash-loop. if ! python3 -m sanad_pkg.license_check "$PKG"; then echo "[$PKG] not licensed for this robot — container exiting cleanly." exit 0 fi # ── 2. resolve config (env wins, then license feature, then config file) ────── read_cfg() { # read_cfg python3 - "$CFG" "$1" <<'PY' 2>/dev/null || true import json, sys try: print(json.load(open(sys.argv[1])).get(sys.argv[2], "") or "") except Exception: print("") PY } # Language: empty = MULTILINGUAL auto-detect (P2's headline feature). Only set a # fixed language if the operator/license/config explicitly pins one. if [ -z "${SANAD_LANGUAGE:-}" ]; then SANAD_LANGUAGE="$(python3 -c 'from sanad_pkg import license as L; print(L.feature("language","") or "")' 2>/dev/null || true)" [ -z "$SANAD_LANGUAGE" ] && SANAD_LANGUAGE="$(read_cfg language_default)" fi export SANAD_LANGUAGE export SANAD_VOICE_BRAIN="${SANAD_VOICE_BRAIN:-gemini}" [ -z "${SANAD_AUDIO_PROFILE:-}" ] && SANAD_AUDIO_PROFILE="$(read_cfg audio_profile_default)" export SANAD_AUDIO_PROFILE="${SANAD_AUDIO_PROFILE:-builtin}" export SANAD_DASHBOARD_HOST="${SANAD_DASHBOARD_HOST:-0.0.0.0}" [ -z "${SANAD_DASHBOARD_PORT:-}" ] && SANAD_DASHBOARD_PORT="$(read_cfg port)" export SANAD_DASHBOARD_PORT="${SANAD_DASHBOARD_PORT:-8013}" export SANAD_MASK_DIR="${SANAD_MASK_DIR:-/app/mask}" export PYTHONUNBUFFERED=1 # Jetson + Unitree SDK OpenMP load-order fix (only if the lib exists; override-able). if [ -z "${LD_PRELOAD:-}" ] && [ -f /usr/lib/aarch64-linux-gnu/libgomp.so.1 ]; then export LD_PRELOAD=/usr/lib/aarch64-linux-gnu/libgomp.so.1 fi echo "[$PKG] entitled — lang=${SANAD_LANGUAGE:-} audio=$SANAD_AUDIO_PROFILE port=$SANAD_DASHBOARD_PORT brain=$SANAD_VOICE_BRAIN mask_dir=$SANAD_MASK_DIR" # ── 3. preflight (clear diagnostics) ───────────────────────────────────────── python3 - "$SANAD_AUDIO_PROFILE" "$SANAD_MASK_DIR" <<'PY' || true import importlib.util as u, sys profile = sys.argv[1] if len(sys.argv) > 1 else "builtin" mask_dir = sys.argv[2] if len(sys.argv) > 2 else "/app/mask" def has(m): return u.find_spec(m) is not None print("[P3] preflight:") ok = sys.version_info >= (3, 9) print(" python : %s %s" % (".".join(map(str, sys.version_info[:3])), "OK" if ok else "TOO OLD — google-genai needs >=3.9")) print(" google-genai : %s" % ("OK" if has("google.genai") else "MISSING — live conversation will NOT work")) print(" pyaudio : %s" % ("OK" if has("pyaudio") else "missing — mic/speaker capture limited")) print(" bleak (mask) : %s" % ("OK" if has("bleak") else "MISSING — LED mask will NOT connect")) print(" Pillow (face) : %s" % ("OK" if has("PIL") else "missing — LifelikeFace falls back to FaceAnimator")) sys.path.insert(0, mask_dir) print(" mask lib : %s (%s)" % ("OK" if has("mask") else "MISSING", mask_dir)) sdk = has("unitree_sdk2py") print(" unitree SDK : %s" % ("OK" if sdk else "absent")) if profile == "builtin" and not sdk: print(" >> NOTE: audio profile 'builtin' (G1 chest) needs the Unitree SDK, which is") print(" absent. Plug a USB speaker/mic and set SANAD_AUDIO_PROFILE=plugged.") PY exec python3 /app/app_p3.py