feat: pod-wide Lyria background music (replaces test-audio drums)

The 'Test audio' button becomes 'Background Music': each pod gets a calm looping track from Gemini Lyria 3 that sings the pod name once up front then stays instrumental. Backend GET /api/pods/:id/music generates via the Gemini interactions endpoint and caches the MP3 per pod in Mongo (pod_music); the frontend fetches and loops it via Web Audio, published pod-wide on the existing podman-beat track.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Kartikeya
2026-06-28 02:55:19 -07:00
parent 604bf9d5ac
commit ab8ea07c12
7 changed files with 218 additions and 43 deletions
+8 -2
View File
@@ -43,6 +43,7 @@ import {
abortLiveConversationHermesJob,
getLiveConversationHermesJob,
getMemberWorkHistory,
podMusicUrl,
startLiveConversation,
stopLiveConversation,
testPodVoice,
@@ -167,7 +168,7 @@ export function PodView({
readStoredBool('podman.teamStreamOpen', true),
);
const { active, hermes, voiceCue, actionUrl, respond } = useInterventions(room);
const { beat, toggleBeat: runBeat } = useBeat(room);
const { beat, toggleBeat: runBeat } = useBeat(room, podMusicUrl(team.id));
const activity = usePodActivity(team.id, me);
const audioRef = useRef<HTMLDivElement>(null);
@@ -177,6 +178,11 @@ export function PodView({
const onLeaveRef = useRef(onLeave);
onLeaveRef.current = onLeave;
// Warm the pod's background-music cache so the first click plays instantly.
useEffect(() => {
void fetch(podMusicUrl(team.id)).catch(() => {});
}, [team.id]);
useEffect(() => {
if (!room) return;
const refresh = () => setParticipants(snapshot(room, me));
@@ -659,7 +665,7 @@ export function PodView({
</Button>
<Button variant="outline" onClick={onToggleBeat} disabled={!room}>
<Volume2Icon data-icon="inline-start" />
{beat.on ? (beat.mine ? 'Stop audio' : `Stop (${beat.by})`) : 'Test audio'}
{beat.on ? (beat.mine ? 'Stop music' : `Stop (${beat.by})`) : 'Background Music'}
</Button>
<Button
variant="outline"