# syntax=docker/dockerfile:1 # ───────────────────────────────────────────────────────────────────────────── # Sanad Package 2 — Premium Communication. # # SELF-CONTAINED: builds from a public base with NO sanad-base / Sanad_Core / sibling # checkout. The engine is vendored from SanadV3 at ./vendor/Sanad, the license/bus # lib at ./vendor/sanad_pkg, and the flat BLE Mask lib at ./vendor/mask. # # Build context MUST be THIS package dir: # docker build -t sanad-p2:latest . # (docker-compose.yml uses context: . ; Jetson Docker without buildx: # DOCKER_BUILDKIT=0 docker build -t sanad-p2:latest .) # ───────────────────────────────────────────────────────────────────────────── ARG BASE_OS_IMAGE=python:3.10-slim-bookworm FROM ${BASE_OS_IMAGE} ENV DEBIAN_FRONTEND=noninteractive \ PYTHONUNBUFFERED=1 \ PYTHONDONTWRITEBYTECODE=1 \ PYTHONPATH=/app WORKDIR /app # System deps: audio (shared) + PortAudio/toolchain (pyaudio) + BlueZ/D-Bus (bleak # talks to the host BlueZ over D-Bus for the LED mask) + iproute2 (`ip`). # `ip` is REQUIRED for chest ('builtin') audio: voice/audio_io.py _find_g1_local_ip() # runs `ip -4 -o addr` to find the host's 192.168.123.x address for the G1 chest-mic # UDP multicast — without it the live voice subprocess crashes on FileNotFoundError. RUN apt-get update && apt-get install -y --no-install-recommends \ ca-certificates libsndfile1 alsa-utils pulseaudio-utils iproute2 \ portaudio19-dev libportaudio2 build-essential python3-dev \ bluez libdbus-1-3 libglib2.0-0 \ && rm -rf /var/lib/apt/lists/* # Python deps (P1 comms + bleak==0.22.3 + Pillow). COPY requirements.txt /tmp/requirements.txt RUN python3 -m pip install --no-cache-dir --upgrade pip \ && python3 -m pip install --no-cache-dir -r /tmp/requirements.txt # ── Optional: Unitree SDK — G1 chest (builtin) audio + arm over DDS ─────────── # WITH_UNITREE_SDK=1 (default) builds CycloneDDS + installs unitree_sdk2_python. # Wrapped so a failure NEVER breaks the image — chest audio is then unavailable # (use SANAD_AUDIO_PROFILE=plugged); plugged USB audio always works. # NOTE: build the FULL CycloneDDS (do NOT pass -DBUILD_IDLC=NO) — the `cyclonedds` # Python binding's find_package(CycloneDDS) needs idlc, else "Could not locate # cyclonedds". Pin the binding to match the 0.10.x C library. ARG WITH_UNITREE_SDK=1 ENV CYCLONEDDS_HOME=/usr/local \ LD_LIBRARY_PATH=/usr/local/lib RUN if [ "$WITH_UNITREE_SDK" = "1" ]; then \ ( set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends git cmake build-essential; \ git clone --depth 1 -b releases/0.10.x https://github.com/eclipse-cyclonedds/cyclonedds /tmp/cyclonedds; \ cmake -S /tmp/cyclonedds -B /tmp/cyclonedds/build -DCMAKE_INSTALL_PREFIX=/usr/local; \ cmake --build /tmp/cyclonedds/build --target install -j"$(nproc)"; \ CYCLONEDDS_HOME=/usr/local CMAKE_PREFIX_PATH=/usr/local python3 -m pip install --no-cache-dir "cyclonedds==0.10.2"; \ git clone --depth 1 https://github.com/unitreerobotics/unitree_sdk2_python /opt/unitree_sdk2_python; \ python3 -m pip install --no-cache-dir -e /opt/unitree_sdk2_python; \ python3 -c "import unitree_sdk2py; print('unitree_sdk2py OK')"; \ rm -rf /tmp/cyclonedds /var/lib/apt/lists/*; \ ) || echo "WARN[P2]: Unitree SDK build failed — chest (builtin) audio unavailable; use SANAD_AUDIO_PROFILE=plugged"; \ else echo "WITH_UNITREE_SDK=0 — skipping Unitree SDK (USB/plugged audio only)"; fi # License/bus shim + PUBLIC verification key (vendored). COPY vendor/sanad_pkg /app/sanad_pkg RUN mkdir -p /etc/sanad && cp /app/sanad_pkg/pubkey.ed25519 /etc/sanad/pubkey.ed25519 # Flat BLE Mask lib on its OWN path (it uses flat imports: `import mask`, `faces`). COPY vendor/mask /app/mask # Canonical engine (vendored from SanadV3 — face/ + evolved voice/audio/arm). COPY vendor/Sanad /app/Sanad # P2 launcher + routes + entrypoint + config. COPY app_p2.py /app/app_p2.py COPY routes_p2.py /app/routes_p2.py COPY entrypoint.sh /app/entrypoint.sh COPY config /app/pkg2_config RUN chmod +x /app/entrypoint.sh # Ship KEYLESS — blank any Gemini key baked into the vendored config. COPY strip_key.py /tmp/strip_key.py RUN python3 /tmp/strip_key.py && rm -f /tmp/strip_key.py # Sanity: vendored namespace + mask lib import paths resolve. RUN python3 - <<'PY' import importlib.util as u, sys ok = all(u.find_spec(m) for m in ("sanad_pkg.license", "sanad_pkg.bus", "Sanad")) sys.path.insert(0, "/app/mask") mask_ok = all(u.find_spec(m) for m in ("mask", "faces", "faceanim")) print("P2 self-contained: vendored Sanad+sanad_pkg importable:", ok, "| mask lib:", mask_ok) sys.exit(0 if (ok and mask_ok) else 1) PY ENV SANAD_PACKAGE=P2 \ SANAD_DASHBOARD_PORT=8012 \ SANAD_DASHBOARD_HOST=0.0.0.0 \ SANAD_MASK_DIR=/app/mask \ SANAD_LICENSE=/etc/sanad/sanad.lic \ SANAD_PUBKEY=/etc/sanad/pubkey.ed25519 EXPOSE 8012 ENTRYPOINT ["/app/entrypoint.sh"]