From bf3e197a20d921702430e3388f8947a8ebe3dca9 Mon Sep 17 00:00:00 2001 From: Lucca Pirovano Date: Thu, 16 Jul 2026 13:29:56 -0400 Subject: [PATCH] simplified code --- README.md | 105 ++++++++++++++++++ ...nd-with-yt-dlp_2026-07-16T17-20-34+0000.md | 56 ++++++++++ naowalk.py | 87 ++++----------- requirements.txt | 19 +++- 4 files changed, 199 insertions(+), 68 deletions(-) create mode 100644 README.md create mode 100644 chat-history/claude_replacing-music-player-backend-with-yt-dlp_2026-07-16T17-20-34+0000.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..5e9cf55 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# naowalk + +A keyboard teleop client for the NAO robot: live video, walking, head control, +two-way audio (hear the robot's mics, make it speak via TTS), and a YouTube +music player that downloads a track with `yt-dlp` and plays it through the +robot's own speakers. + +## Requirements + +**System packages** (Debian/Ubuntu names — adjust for your OS): + +```bash +sudo apt install portaudio19-dev ffmpeg sshpass openssh-client +``` + +- `portaudio19-dev` — needed to build/install `pyaudio` +- `ffmpeg` — needed by `yt-dlp` to extract audio to mp3 +- `sshpass` + `openssh-client` — used to `scp` downloaded tracks onto the + robot and to run `mkdir` over `ssh` (the script assumes the default + `nao`/`nao` credentials) + +**NAOqi Python SDK (`qi`)** — not on PyPI. Download the `pynaoqi-python` +wheel that matches your OS/Python version from the Aldebaran/SoftBank +Robotics developer site and install it: + +```bash +pip install ./pynaoqi-*.whl +``` + +**Python packages:** + +```bash +pip install -r requirements.txt +``` + +## Usage + +```bash +python3 naowalk.py --ip [options] +``` + +| Flag | Default | Description | +|---|---|---| +| `--ip` | `127.0.0.1` | Robot's IP address | +| `--port` | `9559` | NAOqi port | +| `--audio-device-index` | auto | Output device index for hearing the robot's mic on your machine | +| `--mic-channel` | `left` | Which robot mic to stream (`left`, `right`, `front`, `rear`) — pick a working one, see Troubleshooting | +| `--mic-gain` | `8.0` | Amplification applied to the streamed mic audio | +| `--video-scale` | `2.0` | Display window scale (base feed is 320x240) | + +## Controls + +| Key | Action | +|---|---| +| `W`/`A`/`S`/`D` or arrows | Walk forward/turn/back | +| `I`/`K`/`J`/`L` | Head pitch/yaw | +| `Space` | Reset head to center | +| `1` | Wave gesture | +| `M` | Open music search (type query, `Enter` to search, `↑↓` to select, `Enter`/`1`-`5` to play) | +| `P` | Pause / resume music (resume restarts the track from the beginning — see Notes) | +| `X` | Stop music | +| `-` / `=` | Volume down/up | +| `T` | Type a message for the robot to speak (TTS) | +| `Esc` | Quit (or cancel current mode) | + +## How it works + +- **`SoundReceiver`** — registered as a NAOqi service that receives the + robot's mic stream and plays it out your local speakers via `pyaudio`, with + gain and a prebuffer to smooth out network jitter. +- **`NaoMusicPlayer`** — searches YouTube with `yt-dlp`, downloads and + converts the top match to mp3, `scp`s it to the robot, and plays it with + `ALAudioPlayer`. +- **`NaoTeleop`** — the main loop: pygame window for video/input, keyboard + handling, head/walk control, and wiring everything above together. + +## Notes / design decisions + +- **No seek.** Seeking was removed entirely — it added the most complexity + for the least-used feature. `P` is a simple play/stop toggle; resuming + restarts the track from the beginning rather than tracking a playback + position. +- **`moveInit()` on startup.** The robot used to need its walk buttons + jittered for a few seconds before its first walk would work. `run()` now + calls `ALMotionProxy::moveInit()` once at startup, which settles the walk + engine into the right pose before any real move command — this is the + documented NAOqi fix for exactly this symptom. +- **`_safe()` helper.** Most one-off NAOqi calls (setting angles, volume, + stopping movement, unsubscribing services) are wrapped in a small + `self._safe(label, fn)` helper instead of a repeated `try/except` block, so + one failed call logs and moves on without crashing the teleop loop or the + shutdown sequence. + +## Troubleshooting + +- **Mic sounds dead / silent on one channel:** some robots have one or more + physical mics that produce no signal. Test all four with a quick energy + check, then pick a working channel with `--mic-channel`. +- **Robot falls over on the very first walk command:** make sure you're on a + build that includes the `moveInit()` startup call described above; if it's + still happening, increase the `time.sleep(1.0)` after `goToPosture` to give + the posture more time to settle first. +- **`scp`/`ssh` failures for music:** confirm the robot is reachable at + `--ip` and that `nao`/`nao` are still the correct credentials; the script + hardcodes them via `sshpass`. diff --git a/chat-history/claude_replacing-music-player-backend-with-yt-dlp_2026-07-16T17-20-34+0000.md b/chat-history/claude_replacing-music-player-backend-with-yt-dlp_2026-07-16T17-20-34+0000.md new file mode 100644 index 0000000..72391fb --- /dev/null +++ b/chat-history/claude_replacing-music-player-backend-with-yt-dlp_2026-07-16T17-20-34+0000.md @@ -0,0 +1,56 @@ +--- +title: "Replacing music player backend with yt-dlp" +tags: [] +author: claude +count: 2 +exporter: 3.1.0 +date: 2026-07-16T17-20-34+0000 +url: https://claude.ai/chat/8bb5e420-7ad2-4048-be3f-95894713aff6 +--- + +# Replacing music player backend with yt-dlp + +## Table of Contents + +- [1: lets just gut the seek function and focus on what works. we also need…](#chat-1) +- [2: alright awesome, lets remove the start from current position also, i w…](#chat-2) + +## chat-1 + +> lets just gut the seek function and focus on what works. we also need to address the robot falling over if i try to walk from standstill, but only the first time i do it. If i jitter the walk forward and back buttons for about 3 seconds to get over the initial walk issues, it can start walking without falling over. how do we fix this + +Both very solvable. Let's do the walk fix first since it's the more important one — confirmed via NAOqi docs: `ALMotionProxy::moveInit()` is the standard fix for exactly this. It's a one-time blocking call that initializes the walk engine and settles the robot into the right pose _before_ the first real move command, which is exactly what your button-jittering was accidentally doing by brute force. It's missing from the script entirely. + +Now let's gut `seek()` entirely per your request — but keep the position tracking it shares with `pause()`, since that's still needed for resume-from-position to work: + +Right, orphaned body left over. Let's clean that up: + +Now remove the comma/period key bindings that called `seek()`: + +Now compile-check and confirm nothing else references `seek`: + +Clean. Let's ship it: + +___ +###### [top](#table-of-contents) +## chat-2 + +> alright awesome, lets remove the start from current position also, i want to get the lines of code down, what can be done without compromising on the stability and functionality to simplify our code + +Now let's do the full pass. First, the concrete simplification you asked for — remove position tracking, revert pause/resume to restart-from-beginning: + +Now `pause()` becomes a plain stop/restart toggle — no position math, no `playFileFromPosition`: + +Now let's cut the repetitive `try/except Exception as e: print(...)` boilerplate scattered through `NaoTeleop` — same safety, less code, via one small helper: + +Now let's add the `_safe` helper and apply it across these repeated sites: + +Now the `moveInit` startup call: + +Now the shutdown block — this is where it pays off most: + +Let's check the movement block and the `T` key's `stopMove()` call too: + +___ +###### [top](#table-of-contents) + diff --git a/naowalk.py b/naowalk.py index 96b422b..32322e9 100644 --- a/naowalk.py +++ b/naowalk.py @@ -147,9 +147,6 @@ class NaoMusicPlayer: self.current_file = None self.current_task_id = None self.is_playing = False - self.paused_position = 0.0 - self.play_start_time = None - self.position_at_start = 0.0 self.current_title = "" self.is_loading = False self.load_error = None @@ -182,12 +179,6 @@ class NaoMusicPlayer: _DL_PROGRESS_RE = re.compile(r'\[download\]\s+([\d.]+)%') - def _current_position(self): - """Playback position tracked client-side, independent of NAOqi's getCurrentPosition.""" - if self.is_playing and self.play_start_time is not None: - return self.position_at_start + (time.time() - self.play_start_time) - return self.paused_position - def play(self, url, title): self.stop() self.current_title = title @@ -260,9 +251,6 @@ class NaoMusicPlayer: self.progress = 100.0 self.current_task_id = self.player.playFile(self.remote_path) self.is_playing = True - self.play_start_time = time.time() - self.position_at_start = 0.0 - self.paused_position = 0.0 print(f"♪ Playing on NAO: {title} (task {self.current_task_id})") return True except Exception as e: @@ -273,30 +261,24 @@ class NaoMusicPlayer: self.is_loading = False def pause(self): + """Toggle playback. No position tracking: resuming restarts the track from the top.""" if self.current_task_id is None or not self.current_file: print("⏸ Nothing loaded to pause/resume") return if self.is_playing: - pos = self._current_position() try: self.player.stopAll() except Exception as e: print(f"stopAll error while pausing: {e}") self.load_error = f"Pause error: {e}" return - self.paused_position = max(0.0, pos) self.is_playing = False - self.play_start_time = None - print(f"♪ Paused at {self.paused_position:.1f}s") + print("♪ Paused") else: try: - self.current_task_id = self.player.playFileFromPosition( - self.current_file, self.paused_position, 1.0, 0.0 - ) + self.current_task_id = self.player.playFile(self.current_file) self.is_playing = True - self.play_start_time = time.time() - self.position_at_start = self.paused_position - print(f"♪ Resumed from {self.paused_position:.1f}s (task {self.current_task_id})") + print(f"♪ Resumed from start (task {self.current_task_id})") except Exception as e: print(f"Resume error: {e}") self.load_error = f"Resume error: {e}" @@ -310,9 +292,6 @@ class NaoMusicPlayer: self.current_file = None self.current_task_id = None self.current_title = "" - self.paused_position = 0.0 - self.play_start_time = None - self.position_at_start = 0.0 # ========================================== @@ -369,6 +348,14 @@ class NaoTeleop: self.clock = pygame.time.Clock() self.update_title() + def _safe(self, label, fn): + """Run fn(), log+swallow any exception so one failure can't crash the loop.""" + try: + return fn() + except Exception as e: + print(f"{label} error: {e}") + return None + def _init_audio_device(self): try: self.audio_device = self.session.service("ALAudioDevice") @@ -439,18 +426,13 @@ class NaoTeleop: if keys[pygame.K_j]: self.head_yaw = min(self.head_yaw + speed, 2.0); changed = True if keys[pygame.K_l]: self.head_yaw = max(self.head_yaw - speed, -2.0); changed = True if changed: - try: - self.motion.setAngles(["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3) - except Exception as e: - print(f"Head move error: {e}") + self._safe("Head move", lambda: self.motion.setAngles( + ["HeadYaw", "HeadPitch"], [self.head_yaw, self.head_pitch], 0.3)) def reset_head(self): self.head_yaw = 0.0 self.head_pitch = 0.0 - try: - self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4) - except Exception as e: - print(f"Head reset error: {e}") + self._safe("Head reset", lambda: self.motion.setAngles(["HeadYaw", "HeadPitch"], [0.0, 0.0], 0.4)) def update_title(self): mode = "[TYPING] " if self.typing_mode else "" @@ -459,10 +441,7 @@ class NaoTeleop: def change_volume(self, delta): self.volume = int(min(100, max(0, self.volume + delta))) if self.audio_device: - try: - self.audio_device.setOutputVolume(self.volume) - except Exception as e: - print(f"Volume set error: {e}") + self._safe("Volume set", lambda: self.audio_device.setOutputVolume(self.volume)) print(f"🔊 NAO volume: {self.volume}%") self.update_title() @@ -483,10 +462,7 @@ class NaoTeleop: self.motion.setMoveArmsEnabled(True, True) print("🦶 Initializing walk engine...") - try: - self.motion.moveInit() - except Exception as e: - print(f"moveInit error: {e}") + self._safe("moveInit", self.motion.moveInit) print("🦶 Walk engine ready") carpet_config = [["StepHeight", 0.028], ["MaxStepFrequency", 0.6], ["TorsoWy", 0.05]] @@ -585,10 +561,7 @@ class NaoTeleop: self.change_volume(5) elif event.key == pygame.K_t: self.typing_mode = True - try: - self.motion.stopMove() - except Exception as e: - print(f"stopMove error: {e}") + self._safe("stopMove", self.motion.stopMove) if not self.typing_mode and not self.music_search_mode: keys = pygame.key.get_pressed() @@ -679,27 +652,15 @@ class NaoTeleop: print(f"\u26a0\ufe0f Frame error (continuing): {e}") finally: print("🛑 Shutting down...") - try: - self.motion.stopMove() - except Exception as e: - print(f"stopMove error: {e}") - try: - self.music.stop() - except Exception as e: - print(f"music stop error: {e}") + self._safe("stopMove", self.motion.stopMove) + self._safe("music stop", self.music.stop) if self.audio_device: - try: self.audio_device.unsubscribe(self.audio_service_name) - except Exception as e: print(f"audio unsubscribe error: {e}") + self._safe("audio unsubscribe", lambda: self.audio_device.unsubscribe(self.audio_service_name)) if getattr(self, "sound_receiver", None): - try: self.sound_receiver.close() - except Exception as e: print(f"sound receiver close error: {e}") + self._safe("sound receiver close", self.sound_receiver.close) if hasattr(self, 'video') and self.video_client: - try: self.video.unsubscribe(self.video_client) - except Exception as e: print(f"video unsubscribe error: {e}") - try: - self.motion.rest() - except Exception as e: - print(f"rest error: {e}") + self._safe("video unsubscribe", lambda: self.video.unsubscribe(self.video_client)) + self._safe("rest", self.motion.rest) pygame.quit() diff --git a/requirements.txt b/requirements.txt index e0a0688..e1dfb6e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,14 @@ -qi -opencv-python -numpy -pygame -yt-dlp +# Install with: pip install -r requirements.txt +# +# NOTE: the NAOqi Python SDK ("qi") is NOT on PyPI. It ships separately from +# Aldebaran/SoftBank Robotics as a platform-specific wheel (pynaoqi-python*). +# Download it from the Softbank Robotics / Aldebaran developer portal, match +# it to your Python version/OS, and install it with: +# pip install ./pynaoqi-*.whl +# See README.md for details. + +opencv-python>=4.8 +numpy>=1.24 +pygame>=2.5 +pyaudio>=0.2.13 +yt-dlp>=2024.1.1