Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# PepperWizard project env file.
#
# Copy to `.env` at the project root:
# cp .env.example .env
#
# Robot connection.
# Physical robot: set NAOQI_IP to the robot's IP and NAOQI_PORT=9559.
# Simulation: the dev overlay (docker-compose.dev.yml) runs a qiBullet sim
# inside pepper-box:latest. Set NAOQI_IP=127.0.0.1 to trigger sim mode.
NAOQI_IP=192.168.123.50
NAOQI_PORT=9559

# For LLM talk mode, export ANTHROPIC_API_KEY in your shell before running or add to the .env.

# `docker compose ...`. Uncomment to load the dev overlay by default.
# COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
# COMPOSE_PROFILES=gpu

# GPU exposure for stt-service.
# Defaults (both lines unset/commented) → CPU + Whisper.
# To run Parakeet on an NVIDIA host: uncomment both lines below.
# Requires:
# - nvidia-container-toolkit installed on the host
# - the `nvidia` Docker runtime registered (check with `docker info | grep -i runtime`)
# STT_DOCKER_RUNTIME=nvidia
# STT_GPU_DEVICES=all
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ TODO.txt

# Environment and Venv
.venv_test/
robot.env
.env

# Python Packaging
Expand Down
14 changes: 8 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,18 +55,20 @@ This fetches the SDK from Aldebaran's CDN (with a Wayback Machine fallback), ver

### 2. Point PepperWizard at your robot

The connection is configured via a `robot.env` file. Copy the example and edit if your robot isn't at the lab default:
The connection is configured via a `.env` file at the repo root. Copy the example and edit if your robot hasa different IP:

```bash
cp robot.env.example robot.env
cp .env.example .env
```

```bash
# robot.env
# .env
NAOQI_IP=192.168.123.50 # robot IP (use 127.0.0.1 for a local NAOqi sim)
NAOQI_PORT=9559 # 9559 on physical robots, sim-specific otherwise
NAOQI_PORT=9559
```

The same `.env` also includes opt-in switches for GPU STT and dev-overlay shortcuts — see the example for the full list.

### 3. Build and run (MVP)

```bash
Expand Down Expand Up @@ -97,7 +99,7 @@ The overlay mounts both sibling repos into the `pepper-wizard` container for liv

#### Shortening the dev-overlay commands (optional)

Add a `.env` file (gitignored) to the repo root:
Uncomment these lines in your `.env` (created in step 2):

```
COMPOSE_FILE=docker-compose.yml:docker-compose.dev.yml
Expand All @@ -116,7 +118,7 @@ Omit `COMPOSE_PROFILES=gpu` on hosts without NVIDIA runtime.

#### Running against the simulator

Set `NAOQI_IP=127.0.0.1` in `robot.env` to trigger sim mode — `pepper-robot-env`'s entrypoint detects the local IP and boots qiBullet instead of pynaoqi. On first boot it auto-seeds the qiBullet asset cache (Pepper URDF + meshes) into `../PepperBox/.qibullet/`.
Set `NAOQI_IP=127.0.0.1` in `.env` to trigger sim mode — `pepper-robot-env`'s entrypoint detects the local IP and boots qiBullet instead of pynaoqi. On first boot it auto-seeds the qiBullet asset cache (Pepper URDF + meshes) into `../PepperBox/.qibullet/`.

The cache directory is auto-created by Docker as `root`-owned on first mount, which blocks the container's `pepperdev` user (UID 1000) from writing. If the entrypoint prints a permission-denied message, chown the host directory once and recreate the container:

Expand Down
12 changes: 4 additions & 8 deletions docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@
# GPU-heavy services (perception) are gated behind the `gpu` profile so the
# rest of the stack runs on hosts without an NVIDIA runtime. Enable with:
# docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile gpu up
#
# The overlay swaps `pepper-robot-env` from the published NAOqi bridge image to
# the sim-capable `pepper-box:latest` (headless qiBullet) and mounts /dev/dri so
# pybullet's GL renderer works on any open-source driver (AMD, Intel, NVIDIA).

services:
pepper-robot-env:
Expand All @@ -36,11 +32,11 @@ services:
image: ghcr.io/action-prediction-lab/pepper-box:latest
network_mode: host
volumes:
- ./robot.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ./.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ../PepperBox/py3-naoqi-bridge:/home/pepperdev/py3-naoqi-bridge
- ${HOME}/.pepperbox/pynaoqi-python2.7-2.5.7.1-linux64:/opt/pynaoqi-python2.7-2.5.7.1-linux64:ro
env_file:
- robot.env
- .env
environment:
- PYTHONUNBUFFERED=1
command: [ "python2", "/home/pepperdev/py3-naoqi-bridge/state_service.py" ]
Expand All @@ -50,11 +46,11 @@ services:
image: ghcr.io/action-prediction-lab/pepper-box:latest
network_mode: host
volumes:
- ./robot.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ./.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ../PepperBox/py3-naoqi-bridge:/home/pepperdev/py3-naoqi-bridge
- ${HOME}/.pepperbox/pynaoqi-python2.7-2.5.7.1-linux64:/opt/pynaoqi-python2.7-2.5.7.1-linux64:ro
env_file:
- robot.env
- .env
environment:
- PYTHONUNBUFFERED=1
command: [ "python2", "/home/pepperdev/py3-naoqi-bridge/audio_publisher.py" ]
Expand Down
19 changes: 11 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,19 @@
# docker compose -f docker-compose.yml -f docker-compose.dev.yml up
#
# First-run setup:
# ./setup.sh
# cp robot.env.example robot.env && $EDITOR robot.env
# ./setup.sh
# cp .env.example .env && $EDITOR .env
# docker compose up

services:
pepper-robot-env:
image: ghcr.io/action-prediction-lab/pepper-box:latest
network_mode: host
volumes:
- ./robot.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ./.env:/home/pepperdev/py3-naoqi-bridge/robot.env
- ${HOME}/.pepperbox/pynaoqi-python2.7-2.5.7.1-linux64:/opt/pynaoqi-python2.7-2.5.7.1-linux64:ro
env_file:
- robot.env
- .env
environment:
- PYTHONUNBUFFERED=1
command: [ "/home/pepperdev/entrypoint.sh" ]
Expand All @@ -27,7 +27,7 @@ services:
image: ghcr.io/action-prediction-lab/pepper-wizard:latest
build: .
env_file:
- robot.env
- .env
environment:
- HF_HUB_OFFLINE=1
- TRANSFORMERS_OFFLINE=1
Expand All @@ -50,6 +50,7 @@ services:
build:
context: .
dockerfile: stt-service/Dockerfile
runtime: ${STT_DOCKER_RUNTIME:-runc}
network_mode: host
volumes:
- ./stt-service:/app
Expand All @@ -59,13 +60,15 @@ services:
environment:
- PULSE_SERVER=unix:/run/user/1000/pulse/native
- PYTHONUNBUFFERED=1
- HF_HOME=/opt/whisper-cache
- HF_HOME=/opt/asr-cache
- NVIDIA_VISIBLE_DEVICES=${STT_GPU_DEVICES:-none}
- NVIDIA_DRIVER_CAPABILITIES=compute,utility
healthcheck:
# Round-trips a real ZMQ ping so pepper-wizard only starts after STT
# is answering on :5562 (and Whisper model is loaded).
# is answering on :5562 (and the backend model is loaded).
test: ["CMD", "python", "-c", "import sys, zmq; ctx=zmq.Context.instance(); s=ctx.socket(zmq.REQ); s.setsockopt(zmq.RCVTIMEO,2000); s.setsockopt(zmq.SNDTIMEO,2000); s.setsockopt(zmq.LINGER,0); s.connect('tcp://localhost:5562'); s.send_json({'action':'ping'}); sys.exit(0 if s.recv_json().get('status')=='ok' else 1)"]
interval: 2s
timeout: 3s
retries: 15
start_period: 30s
start_period: 60s
restart: on-failure
3 changes: 3 additions & 0 deletions pepper_wizard/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,13 +804,15 @@ def llm_talk_session(robot_client, config, verbose=False):
print_formatted_text(HTML(
"<ansired>Error: Could not connect to the STT service.</ansired>"
))
logger.error("STTEnableFailed", {"phase": "ping"})
stt_client.close()
return

if not stt_client.enable_streaming():
print_formatted_text(HTML(
"<ansired>Error: Could not enable streaming on STT service.</ansired>"
))
logger.error("STTEnableFailed", {"phase": "enable_streaming"})
stt_client.close()
return
logger.info("StreamingEnabled", {})
Expand Down Expand Up @@ -910,6 +912,7 @@ def _handle_vad_event(evt, *, review_mode, llm, stt, robot_client, logger, sessi
print_formatted_text(
HTML("<ansired>[VAD error] {}: {}</ansired>").format(evt.get("error", ""), evt.get("detail", ""))
)
logger.error("VADError", {"error": evt.get("error", ""), "detail": evt.get("detail", "")})
return

text = (evt.get("text") or "").strip()
Expand Down
4 changes: 3 additions & 1 deletion pepper_wizard/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,9 +103,11 @@ def load_llm_config(file_path):
def load_stt_config(file_path):
"""Loads speech-to-text configuration from a JSON file."""
defaults = {
"engine": "whisper",
"zmq_address": "tcp://localhost:5562",
"review_mode": True,
"model_size": "base.en",
"whisper_model": "base.en",
"parakeet_model": "nvidia/parakeet-tdt-0.6b-v2",
"sample_rate": 16000,
"push_to_talk_key": "space",
}
Expand Down
4 changes: 3 additions & 1 deletion pepper_wizard/config/stt.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
{
"engine": "parakeet",
"zmq_address": "tcp://localhost:5562",
"audio_zmq_address": "tcp://localhost:5563",
"transcription_zmq_address": "tcp://localhost:5564",
"review_mode": true,
"llm_review_mode": true,
"model_size": "base.en",
"whisper_model": "base.en",
"parakeet_model": "nvidia/parakeet-tdt-0.6b-v2",
"sample_rate": 16000,
"push_to_talk_key": "space",
"vad": {
Expand Down
2 changes: 1 addition & 1 deletion pepper_wizard/probe/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ def main(argv: Optional[Sequence[str]] = None) -> int:
)
parser.add_argument(
"--robot-env", type=str, default=None,
help="Path to robot.env (default: ./robot.env if present).",
help="Path to the project env file (default: ./.env if present).",
)
args = parser.parse_args(argv)

Expand Down
4 changes: 2 additions & 2 deletions pepper_wizard/probe/detect.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ def detect_robot(
) -> DetectorResult:
raw = {}
if ip is None or port is None:
env_path = Path(robot_env_path) if robot_env_path else Path("robot.env")
env_path = Path(robot_env_path) if robot_env_path else Path(".env")
raw["robot_env_path"] = str(env_path)
raw["robot_env_exists"] = env_path.exists()
if env_path.exists():
Expand All @@ -108,7 +108,7 @@ def detect_robot(
if not ip or not port:
return DetectorResult(
value="missing-config",
detail="NAOQI_IP/NAOQI_PORT not set (no robot.env or incomplete)",
detail="NAOQI_IP/NAOQI_PORT not set (no .env or incomplete)",
raw=raw,
)

Expand Down
2 changes: 1 addition & 1 deletion pepper_wizard/probe/profile.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def probe(cls, robot_env_path: Optional[str] = None) -> "Profile":
def recommend(self) -> Recommendation:
missing = []
if self.robot.value == "missing-config":
missing.append("Robot endpoint not configured — set NAOQI_IP/NAOQI_PORT in robot.env")
missing.append("Robot endpoint not configured — set NAOQI_IP/NAOQI_PORT in .env")
elif self.robot.value == "unreachable":
missing.append(f"Robot unreachable — {self.robot.detail}")
if self.audio.value == "none":
Expand Down
6 changes: 6 additions & 0 deletions pepper_wizard/stt_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

import zmq

from .logger import get_logger


class STTClient:
"""Thin wrapper around the ZMQ REQ socket to the STT service."""
Expand All @@ -23,6 +25,7 @@ def __init__(self, zmq_address: str, timeout_ms: int = 30000):
self.socket.setsockopt(zmq.LINGER, 0)
self.socket.connect(zmq_address)
self._connected = False
self._log = get_logger("STTClient")

def ping(self) -> bool:
"""Check if the STT service is reachable."""
Expand All @@ -46,6 +49,7 @@ def start_recording(self) -> bool:
return reply.get("status") == "recording"
except zmq.ZMQError as e:
print(f"[STTClient] Start error: {e}")
self._log.error("STTClientError", {"action": "start", "error": str(e)})
return False

def stop_and_transcribe(self) -> dict:
Expand All @@ -64,6 +68,7 @@ def stop_and_transcribe(self) -> dict:
return reply
except zmq.ZMQError as e:
print(f"[STTClient] Stop/transcribe error: {e}")
self._log.error("STTClientError", {"action": "stop", "error": str(e)})
return {"transcription": "", "error": str(e)}

def enable_streaming(self) -> bool:
Expand Down Expand Up @@ -111,6 +116,7 @@ def _simple_action(self, action: str, expected_status: str) -> bool:
return reply.get("status") == expected_status
except zmq.ZMQError as e:
print(f"[STTClient] {action} error: {e}")
self._log.error("STTClientError", {"action": action, "error": str(e)})
return False

@property
Expand Down
15 changes: 0 additions & 15 deletions robot.env.example

This file was deleted.

20 changes: 8 additions & 12 deletions stt-service/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
FROM python:3.9-slim
FROM python:3.11-slim

WORKDIR /app

# Install system dependencies for sounddevice (PortAudio) and PulseAudio
RUN apt-get update && apt-get install -y \
libportaudio2 \
libsndfile1 \
libpulse0 \
libasound2-plugins \
pulseaudio-utils \
ffmpeg \
&& rm -rf /var/lib/apt/lists/*

# Configure ALSA to route through PulseAudio (PortAudio uses ALSA backend)
Expand All @@ -21,19 +21,15 @@ RUN echo '\
}\n' > /etc/asound.conf

COPY stt-service/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
RUN pip install --no-cache-dir --index-url https://download.pytorch.org/whl/cu124 torch torchaudio \
&& pip install --no-cache-dir -r requirements.txt

# Bake the Whisper model into the image so first-boot is fast and offline-capable.
# Reads model_size from pepper_wizard/config/stt.json
# shared with the runtime config (mounted at /app/pepper_config/stt.json).
# HF_HOME points outside /app so the runtime bind-mount of ./stt-service:/app
ENV HF_HOME=/opt/whisper-cache

ENV HF_HOME=/opt/asr-cache
COPY pepper_wizard/config/stt.json /tmp/stt.json
COPY stt-service/bake_models.py stt-service/config_loader.py stt-service/vad_segmenter.py /tmp/
RUN mkdir -p ${HF_HOME} && \
python -c "import json; from faster_whisper import WhisperModel; \
m = json.load(open('/tmp/stt.json')).get('model_size', 'base.en'); \
print(f'[build] baking whisper model: {m}'); \
WhisperModel(m, device='cpu', compute_type='int8')"
PYTHONPATH=/tmp python -u /tmp/bake_models.py /tmp/stt.json

COPY stt-service/. .

Expand Down
Loading
Loading