feat(voice): on-demand PodMan voice test + Hermes notify + TTS tuning

Snapshot of in-progress voice work, committed to unblock concurrent edits: speakInRoom + POST voice-test endpoint, Test PodMan voice button (PodView/api), Hermes notify action + scripts/hermes-notify.mjs, agent exits on LiveKit disconnect for auto-restart, TTS playback tuning (subscriber-ready delay, preroll/tail silence, fallback line, microphone source), verify script updates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Yahya Alhinai
2026-06-28 06:45:37 +00:00
parent 7269098ea3
commit c1ac687dcf
12 changed files with 438 additions and 31 deletions
+53
View File
@@ -1,6 +1,10 @@
import type { Room } from '@livekit/rtc-node';
import { Room as LiveKitRoom } from '@livekit/rtc-node';
import { AccessToken } from 'livekit-server-sdk';
import type { Collision, DataMessage, HermesMessage, Intervention } from '@podman/shared';
import { DATA_TOPIC } from '@podman/shared';
import { env } from '../env.js';
import { speak } from '../voice/live.js';
const encoder = new TextEncoder();
@@ -37,3 +41,52 @@ export async function publishHermesMessage(
topic: DATA_TOPIC,
});
}
export async function publishHermesIntervention(
room: Room,
collision: Collision,
intervention: Intervention,
voiceLine?: string,
): Promise<void> {
const data: DataMessage = { type: 'COLLISION', collision, intervention };
await room.localParticipant?.publishData(encoder.encode(JSON.stringify(data)), {
reliable: true,
topic: DATA_TOPIC,
});
await publishHermesMessage(room, collision, intervention);
if (voiceLine) await speak(room, voiceLine);
}
async function hermesToken(roomName: string): Promise<string> {
const at = new AccessToken(env.LIVEKIT_API_KEY, env.LIVEKIT_API_SECRET, {
identity: `podman-hermes-${Date.now()}`,
name: 'PodMan Hermes',
ttl: '10m',
});
at.addGrant({
roomJoin: true,
room: roomName,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
return at.toJwt();
}
export async function notifyHermesInterventionInRoom(
roomName: string,
collision: Collision,
intervention: Intervention,
voiceLine?: string,
): Promise<void> {
const room = new LiveKitRoom();
try {
await room.connect(env.LIVEKIT_URL, await hermesToken(roomName), {
autoSubscribe: false,
dynacast: false,
});
await publishHermesIntervention(room, collision, intervention, voiceLine);
} finally {
await room.disconnect().catch(() => {});
}
}