OMG, it works
This commit is contained in:
parent
962dd3fb70
commit
4ecfd380c1
|
|
@ -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.
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
const playedSounds = new Set<string>()
|
this.#startSpeakingSounds()
|
||||||
let lastSoundDir: SoundDir | undefined
|
}
|
||||||
|
|
||||||
|
async #startTypingSounds() {
|
||||||
|
return new Promise<void>(async (resolve) => {
|
||||||
do {
|
do {
|
||||||
const dir = this.getRandomSoundDir(lastSoundDir)
|
const value = Math.random() * 100
|
||||||
const soundPath = getSound(dir, Array.from(playedSounds))
|
let dir: SoundDir
|
||||||
playedSounds.add(soundPath)
|
if (value > 33) {
|
||||||
lastSoundDir = dir
|
dir = "typing"
|
||||||
console.log(`🌭 playing ${soundPath}`)
|
} else {
|
||||||
|
dir = "clicking"
|
||||||
|
}
|
||||||
|
const typingSound = getSound(dir)
|
||||||
|
this.typingPlayback = await this.player.play(typingSound)
|
||||||
|
await this.typingPlayback.finished()
|
||||||
|
} while (this.typingPlayback)
|
||||||
|
|
||||||
const playback = await this.player.play(soundPath)
|
resolve()
|
||||||
this.currentPlayback = playback
|
})
|
||||||
await playback.finished()
|
}
|
||||||
} while (this.currentPlayback)
|
|
||||||
|
async #startSpeakingSounds() {
|
||||||
|
const playedSounds = new Set<string>()
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
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()
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user