feat: progressive music streaming, library-first search, player fixes
Rebuild & Deploy / rebuild (push) Successful in 59s

- Stream downloads as they grow: raw best-audio (no mp3 transcode) served
  chunked from the stream endpoint mid-download, then with native Range
  once complete. Songs play within seconds of hitting Play.
- Fix searched songs never starting: reconcileMedia now wires the stream
  URL when a track that was preparing gains one; audio_url is stable for
  the whole lifecycle so the player never reloads mid-playback.
- Progress bars now parse yt-dlp stderr (stdout never carried [download]).
- Prefer downloaded songs: /media/api/search merges ready library matches
  ahead of YouTube results; search_music/play_song tooling and the agent
  prompt tell Hope to always pick in_library=true.
- Mute button turns solid red while the mic is muted.
This commit is contained in:
snowspeeder committed 2026-08-29 16:41:38 -04:00
1 parent d89918fd2a
commit 4f8eb00c9f
7 files changed
+440 -107

No files matched your search

+7 -4
View File
@@ -213,10 +213,13 @@ SYSTEM_PROMPT = textwrap.dedent("""\
- Ambiguous results → offer_song_choices with the top 2-3 options, name
them briefly, and let the user pick by voice or in the player panel.
Never start playback on a guess.
The library is permanent: list_library shows what is already downloaded,
and those tracks play instantly without searching. If the user names a
library track exactly, just play it. Control playback with pause_music,
resume_music, skip_song and seek_music; check what's on with now_playing.
Prefer what is already saved: search_music puts downloaded library
matches first (in_library=true) — always pick one of those over a fresh
YouTube result, and list_library shows the whole saved collection which
plays instantly. If the user names a library track exactly, just play it.
A brand-new song starts playing within seconds and finishes downloading
in the background. Control playback with pause_music, resume_music,
skip_song and seek_music; check what's on with now_playing.
add_stream registers a live radio or direct audio URL as a library track.
While music is playing you cannot hear the user — they will come back to
you when they want to talk again, and the music pauses automatically.
+16 -13
View File
@@ -69,27 +69,30 @@ def _fmt_duration(seconds) -> str:
@mcp.tool()
def search_music(query: str, limit: int = 5) -> str:
"""Search YouTube Music for songs. Call this before playing anything the
user describes by name, artist or lyrics. Returns JSON results with
video_id, title, channel, duration and in_library flags. Pick the result
that clearly matches what the user asked for; if two or more could be
right, use offer_song_choices instead of guessing."""
"""Search for songs: already-downloaded library matches come first, then
YouTube Music results. Call this before playing anything the user
describes by name, artist or lyrics. Results carry video_id, title,
channel, duration and in_library flags. ALWAYS prefer a result with
in_library=true — it is already saved and plays instantly. Pick the
result that clearly matches what the user asked for; if two or more could
be right, use offer_song_choices instead of guessing."""
data = _media_post("/media/api/search", {"query": query, "limit": max(1, min(limit, 10))})
if "error" in data:
return f"Search failed: {data['error']}"
results = data.get("results", [])
if not results:
return f"No YouTube Music results for '{query}'."
return f"No results for '{query}'."
return json.dumps(results, ensure_ascii=False)
@mcp.tool()
def play_song(video_id: str, title: str = "", channel: str = "") -> str:
"""Play a song in the user's web player. Pass the video_id from
search_music (or an in_library track_id). Downloads and transcodes first
if needed; playback starts automatically once ready. After calling this,
tell the user what is playing in one short sentence and stop talking —
the player takes over and the conversation pauses until they return."""
search_music — for an in_library result this plays the saved copy with no
download; a new song downloads and starts playing automatically while it
arrives. After calling this, tell the user what is playing in one short
sentence and stop talking — the player takes over and the conversation
pauses until they return."""
data = _media_post("/media/api/play", {
"room": MEDIA_ROOM, "video_id": video_id, "title": title, "channel": channel,
})
@@ -100,9 +103,9 @@ def play_song(video_id: str, title: str = "", channel: str = "") -> str:
name = track.get("title") or title or video_id
if status == "downloading":
return (
f"'{name}' is downloading and will start playing automatically when "
"ready (usually under a minute). Say it's on the way, then hand over "
"to the player."
f"'{name}' is downloading and starts playing within seconds — "
"audio streams in while the download finishes. Say it's on the "
"way, then hand over to the player."
)
return f"Playing '{name}' now. Hand over to the player with a short sign-off."