103 lines
5.3 KiB
Docker
103 lines
5.3 KiB
Docker
# 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"]
|