OMG, it works

This commit is contained in:
Corey Johnson 2025-11-17 13:01:24 -08:00
parent 962dd3fb70
commit 4ecfd380c1
19 changed files with 89 additions and 58 deletions

View File

@ -20,7 +20,7 @@ await $`ssh ${PI_HOST} "mkdir -p ${PI_DIR}"`
// Sync files from . directory to Pi (only transfers changed files) // Sync files from . directory to Pi (only transfers changed files)
console.log("Syncing files from . directory...") console.log("Syncing files from . directory...")
await $`rsync -avz --delete --exclude-from='.gitignore' . ${PI_HOST}:${PI_DIR}/` await $`rsync -avz --delete --exclude-from='../.gitignore' --exclude='.git' . ${PI_HOST}:${PI_DIR}/`
// Make all TypeScript files executable // Make all TypeScript files executable
console.log("Making scripts executable...") console.log("Making scripts executable...")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -26,5 +26,7 @@ export const searchWeb = async (query: string) => {
input: `Search the web for: ${query}`, input: `Search the web for: ${query}`,
}) })
await Bun.sleep(10000)
return response.output_text return response.output_text
} }

View File

@ -151,13 +151,26 @@ export class Player {
stderr: "pipe", stderr: "pipe",
}) })
let bufferFinishTime = performance.now()
const format = this.#format
const bytesPerSecond = format.sampleRate * format.channels * 2
const handle: StreamingPlayback = { const handle: StreamingPlayback = {
isPlaying: true, isPlaying: true,
write: (chunk: Uint8Array) => { write: (chunk: Uint8Array) => {
if (handle.isPlaying) { if (handle.isPlaying) {
proc.stdin.write(chunk) proc.stdin.write(chunk)
const chunkDurationMs = (chunk.byteLength / bytesPerSecond) * 1000
bufferFinishTime = Math.max(performance.now(), bufferFinishTime) + chunkDurationMs
} }
}, },
get bufferEmptyFor() {
const now = performance.now()
if (now > bufferFinishTime) {
return now - bufferFinishTime
}
return 0
},
stop: async () => { stop: async () => {
if (!handle.isPlaying) return if (!handle.isPlaying) return
handle.isPlaying = false handle.isPlaying = false

View File

@ -33,6 +33,7 @@ export type StreamingPlayback = {
isPlaying: boolean; isPlaying: boolean;
write: (chunk: Uint8Array) => void; write: (chunk: Uint8Array) => void;
stop: () => Promise<void>; stop: () => Promise<void>;
bufferEmptyFor: number; // milliseconds since buffer became empty, 0 if not empty
}; };
// Streaming recording control handle // Streaming recording control handle

View File

@ -21,8 +21,8 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
let currentDialtone: Playback | undefined let currentDialtone: Playback | undefined
let currentBackgroundNoise: Playback | undefined let currentBackgroundNoise: Playback | undefined
let currentPlayback = player.playStream() let streamPlayback = player.playStream()
const waitingIndicator = new WaitingSounds(player) const waitingIndicator = new WaitingSounds(player, streamPlayback)
// Set up agent event listeners // Set up agent event listeners
agent.events.connect((event) => { agent.events.connect((event) => {
@ -42,13 +42,13 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
case "audio": case "audio":
waitingIndicator.stop() waitingIndicator.stop()
const audioBuffer = Buffer.from(event.audioBase64, "base64") const audioBuffer = Buffer.from(event.audioBase64, "base64")
currentPlayback.write(audioBuffer) streamPlayback.write(audioBuffer)
break break
case "interruption": case "interruption":
console.log("🛑 User interrupted") console.log("🛑 User interrupted")
currentPlayback?.stop() streamPlayback?.stop()
currentPlayback = player.playStream() // Reset playback stream streamPlayback = player.playStream() // Reset playback stream
break break
case "tool_call": case "tool_call":
@ -66,7 +66,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
case "disconnected": case "disconnected":
console.log("\n👋 Conversation ended, returning to dialtone\n") console.log("\n👋 Conversation ended, returning to dialtone\n")
currentPlayback?.stop() streamPlayback?.stop()
state = "WAITING_FOR_VOICE" state = "WAITING_FOR_VOICE"
startDialtone() startDialtone()
break break
@ -94,9 +94,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
const stopDialtone = async () => { const stopDialtone = async () => {
await currentDialtone?.stop() await currentDialtone?.stop()
currentDialtone = undefined currentDialtone = undefined
currentBackgroundNoise = await player.play(getSound("background"), { currentBackgroundNoise = await player.play(getSound("background"), { repeat: true })
repeat: true,
})
} }
const startConversation = async () => { const startConversation = async () => {
@ -139,7 +137,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
console.log("\n\n🛑 Shutting down phone system...") console.log("\n\n🛑 Shutting down phone system...")
await currentDialtone?.stop() await currentDialtone?.stop()
await currentBackgroundNoise?.stop() await currentBackgroundNoise?.stop()
await currentPlayback?.stop() await streamPlayback?.stop()
await agent.stop() await agent.stop()
process.exit(0) process.exit(0)
} }

View File

@ -1,57 +1,80 @@
import Buzz, { type Player } from "../buzz/index.ts" import { type Player } from "../buzz/index.ts"
import { join } from "path" import { join } from "path"
import type { Playback } from "../buzz/utils.ts" import type { Playback, StreamingPlayback } from "../buzz/utils.ts"
import { random } from "./index.ts" import { random } from "./index.ts"
export class WaitingSounds { export class WaitingSounds {
currentPlayback?: Playback typingPlayback?: Playback
speakingPlayback?: Playback
constructor(private player: Player) {} constructor(private player: Player, private streamPlayback: StreamingPlayback) {}
async start() { async start() {
if (this.currentPlayback) return // Already playing if (this.typingPlayback) return // Already playing
// Now play randomly play things this.#startTypingSounds()
this.#startSpeakingSounds()
}
async #startTypingSounds() {
return new Promise<void>(async (resolve) => {
do {
const value = Math.random() * 100
let dir: SoundDir
if (value > 33) {
dir = "typing"
} else {
dir = "clicking"
}
const typingSound = getSound(dir)
this.typingPlayback = await this.player.play(typingSound)
await this.typingPlayback.finished()
} while (this.typingPlayback)
resolve()
})
}
async #startSpeakingSounds() {
const playedSounds = new Set<string>() const playedSounds = new Set<string>()
let lastSoundDir: SoundDir | undefined let dir: SoundDir | undefined
do { return new Promise<void>(async (resolve) => {
const dir = this.getRandomSoundDir(lastSoundDir) // Don't start speaking until the stream playback buffer is empty!
const soundPath = getSound(dir, Array.from(playedSounds)) while (this.streamPlayback.bufferEmptyFor < 1000) {
playedSounds.add(soundPath) await Bun.sleep(100)
lastSoundDir = dir }
console.log(`🌭 playing ${soundPath}`)
const playback = await this.player.play(soundPath) do {
this.currentPlayback = playback this.streamPlayback.bufferEmptyFor
await playback.finished() const lastSoundDir = dir
} while (this.currentPlayback) const value = Math.random() * 100
if (lastSoundDir === "body-noises") {
dir = "apology"
} else if (value > 99 && !lastSoundDir) {
dir = "body-noises"
} else if (value > 75 && !lastSoundDir) {
dir = "stalling"
} else {
dir = undefined
await Bun.sleep(1000)
}
if (dir) {
const speakingSound = getSound(dir, Array.from(playedSounds))
this.speakingPlayback = await this.player.play(speakingSound)
playedSounds.add(speakingSound)
await this.speakingPlayback.finished()
}
} while (this.typingPlayback)
resolve()
})
} }
async stop() { async stop() {
if (!this.currentPlayback) return if (!this.typingPlayback) return
await this.currentPlayback.finished()
this.currentPlayback = undefined
}
getRandomSoundDir(lastSoundDir?: SoundDir): SoundDir { await Promise.all([this.typingPlayback.stop(), this.speakingPlayback?.finished()])
if (lastSoundDir === "body-noises") { this.typingPlayback = undefined
return "apology"
}
const skipSpecialSounds =
(lastSoundDir !== "typing" && lastSoundDir !== "clicking") || !lastSoundDir
const value = Math.random() * 100
console.log(`🎲 got ${value}`)
if (value > 95 && !skipSpecialSounds) {
return "body-noises"
} else if (value > 66 && !skipSpecialSounds) {
return "stalling"
} else if (value > 33) {
return "clicking"
} else {
return "typing"
}
} }
} }
@ -78,9 +101,3 @@ export const getSound = (dir: SoundDir, exclude: string[] = []): string => {
return random(filteredSoundPaths) return random(filteredSoundPaths)
} }
const player = await Buzz.defaultPlayer()
Buzz.setVolume(0.2)
player.play(getSound("background"), { repeat: true })
const x = new WaitingSounds(player)
x.start()