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

2
.gitignore vendored
View File

@ -31,4 +31,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
.idea
# Finder (MacOS) folder config
.DS_Store
.DS_Store

View File

@ -20,7 +20,7 @@ await $`ssh ${PI_HOST} "mkdir -p ${PI_DIR}"`
// Sync files from . directory to Pi (only transfers changed files)
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
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}`,
})
await Bun.sleep(10000)
return response.output_text
}

View File

@ -151,13 +151,26 @@ export class Player {
stderr: "pipe",
})
let bufferFinishTime = performance.now()
const format = this.#format
const bytesPerSecond = format.sampleRate * format.channels * 2
const handle: StreamingPlayback = {
isPlaying: true,
write: (chunk: Uint8Array) => {
if (handle.isPlaying) {
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 () => {
if (!handle.isPlaying) return
handle.isPlaying = false

View File

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

View File

@ -21,8 +21,8 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
let currentDialtone: Playback | undefined
let currentBackgroundNoise: Playback | undefined
let currentPlayback = player.playStream()
const waitingIndicator = new WaitingSounds(player)
let streamPlayback = player.playStream()
const waitingIndicator = new WaitingSounds(player, streamPlayback)
// Set up agent event listeners
agent.events.connect((event) => {
@ -42,13 +42,13 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
case "audio":
waitingIndicator.stop()
const audioBuffer = Buffer.from(event.audioBase64, "base64")
currentPlayback.write(audioBuffer)
streamPlayback.write(audioBuffer)
break
case "interruption":
console.log("🛑 User interrupted")
currentPlayback?.stop()
currentPlayback = player.playStream() // Reset playback stream
streamPlayback?.stop()
streamPlayback = player.playStream() // Reset playback stream
break
case "tool_call":
@ -66,7 +66,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
case "disconnected":
console.log("\n👋 Conversation ended, returning to dialtone\n")
currentPlayback?.stop()
streamPlayback?.stop()
state = "WAITING_FOR_VOICE"
startDialtone()
break
@ -94,9 +94,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
const stopDialtone = async () => {
await currentDialtone?.stop()
currentDialtone = undefined
currentBackgroundNoise = await player.play(getSound("background"), {
repeat: true,
})
currentBackgroundNoise = await player.play(getSound("background"), { repeat: true })
}
const startConversation = async () => {
@ -139,7 +137,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
console.log("\n\n🛑 Shutting down phone system...")
await currentDialtone?.stop()
await currentBackgroundNoise?.stop()
await currentPlayback?.stop()
await streamPlayback?.stop()
await agent.stop()
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 type { Playback } from "../buzz/utils.ts"
import type { Playback, StreamingPlayback } from "../buzz/utils.ts"
import { random } from "./index.ts"
export class WaitingSounds {
currentPlayback?: Playback
typingPlayback?: Playback
speakingPlayback?: Playback
constructor(private player: Player) {}
constructor(private player: Player, private streamPlayback: StreamingPlayback) {}
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>()
let lastSoundDir: SoundDir | undefined
do {
const dir = this.getRandomSoundDir(lastSoundDir)
const soundPath = getSound(dir, Array.from(playedSounds))
playedSounds.add(soundPath)
lastSoundDir = dir
console.log(`🌭 playing ${soundPath}`)
let dir: SoundDir | undefined
return new Promise<void>(async (resolve) => {
// Don't start speaking until the stream playback buffer is empty!
while (this.streamPlayback.bufferEmptyFor < 1000) {
await Bun.sleep(100)
}
const playback = await this.player.play(soundPath)
this.currentPlayback = playback
await playback.finished()
} while (this.currentPlayback)
do {
this.streamPlayback.bufferEmptyFor
const lastSoundDir = dir
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() {
if (!this.currentPlayback) return
await this.currentPlayback.finished()
this.currentPlayback = undefined
}
if (!this.typingPlayback) return
getRandomSoundDir(lastSoundDir?: SoundDir): SoundDir {
if (lastSoundDir === "body-noises") {
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"
}
await Promise.all([this.typingPlayback.stop(), this.speakingPlayback?.finished()])
this.typingPlayback = undefined
}
}
@ -78,9 +101,3 @@ export const getSound = (dir: SoundDir, exclude: string[] = []): string => {
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()