Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 953fb3aff1 | |||
| 96d10c3df0 | |||
| 616d4472d2 |
214
scripts/cli.ts
214
scripts/cli.ts
|
|
@ -5,174 +5,126 @@ import {$} from "bun";
|
|||
const SERVICES = {
|
||||
ap: "phone-ap",
|
||||
web: "phone-web",
|
||||
};
|
||||
phone: "phone",
|
||||
} as const;
|
||||
|
||||
const commands = {
|
||||
status: "Show status of all services",
|
||||
logs: "Show recent logs from all services (last 50 lines)",
|
||||
tail: "Tail logs from all services in real-time",
|
||||
restart: "Restart all services",
|
||||
stop: "Stop all services",
|
||||
start: "Start all services",
|
||||
"ap-status": "Show status of AP service",
|
||||
"ap-logs": "Show recent logs from AP service (last 50 lines)",
|
||||
"ap-tail": "Tail logs from AP service in real-time",
|
||||
"ap-restart": "Restart AP service",
|
||||
"ap-stop": "Stop AP service",
|
||||
"ap-start": "Start AP service",
|
||||
"web-status": "Show status of web service",
|
||||
"web-logs": "Show recent logs from web service (last 50 lines)",
|
||||
"web-tail": "Tail logs from web service in real-time",
|
||||
"web-restart": "Restart web service",
|
||||
"web-stop": "Stop web service",
|
||||
"web-start": "Start web service",
|
||||
help: "Show this help message",
|
||||
};
|
||||
const COMMANDS = {
|
||||
status: "Show service status",
|
||||
logs: "Show recent logs (last 50 lines)",
|
||||
tail: "Tail logs in real-time",
|
||||
restart: "Restart service (requires sudo)",
|
||||
stop: "Stop service (requires sudo)",
|
||||
start: "Start service (requires sudo)",
|
||||
} as const;
|
||||
|
||||
const command = process.argv[2];
|
||||
|
||||
if (!command || command === "help") {
|
||||
const showHelp = () => {
|
||||
console.log(`
|
||||
Phone CLI - Service Management Tool
|
||||
Phone CLI - Service Management
|
||||
|
||||
Usage: bun cli <command>
|
||||
Usage: cli SERVICE COMMAND [-v]
|
||||
|
||||
All Services:
|
||||
status Show status of all services
|
||||
logs Show recent logs from all services (last 50 lines)
|
||||
tail Tail logs from all services in real-time
|
||||
restart Restart all services
|
||||
stop Stop all services
|
||||
start Start all services
|
||||
Services:
|
||||
ap WiFi AP Monitor (phone-ap.service)
|
||||
web Web Server (phone-web.service)
|
||||
phone Phone Application (phone.service)
|
||||
|
||||
AP Service (phone-ap):
|
||||
ap-status Show AP status
|
||||
ap-logs Show AP logs (last 50 lines)
|
||||
ap-tail Tail AP logs in real-time
|
||||
ap-restart Restart AP service
|
||||
ap-stop Stop AP service
|
||||
ap-start Start AP service
|
||||
Commands:
|
||||
status Show service status
|
||||
logs Show recent logs (last 50 lines)
|
||||
tail Tail logs in real-time
|
||||
restart Restart service (requires sudo)
|
||||
stop Stop service (requires sudo)
|
||||
start Start service (requires sudo)
|
||||
|
||||
Web Service (phone-web):
|
||||
web-status Show web status
|
||||
web-logs Show web logs (last 50 lines)
|
||||
web-tail Tail web logs in real-time
|
||||
web-restart Restart web service
|
||||
web-stop Stop web service
|
||||
web-start Start web service
|
||||
Options:
|
||||
-v Verbose mode - show actual systemd commands
|
||||
|
||||
Examples:
|
||||
bun cli status
|
||||
bun cli ap-logs
|
||||
bun cli web-tail
|
||||
sudo bun cli ap-restart
|
||||
cli ap status
|
||||
cli web logs
|
||||
cli phone tail
|
||||
cli -v ap status
|
||||
sudo cli ap restart
|
||||
`);
|
||||
};
|
||||
|
||||
// Parse arguments
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
// Check for help
|
||||
if (args.length === 0 || args[0] === "help") {
|
||||
showHelp();
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!Object.keys(commands).includes(command)) {
|
||||
console.error(`❌ Unknown command: ${command}`);
|
||||
console.log(`Run 'bun cli.ts help' to see available commands`);
|
||||
// Extract verbose flag and remaining args
|
||||
const verbose = args.includes("-v");
|
||||
const [service, command] = args.filter(arg => arg !== "-v");
|
||||
|
||||
// Validate service
|
||||
if (!service || !(service in SERVICES)) {
|
||||
console.error(`❌ Unknown service: ${service || "(missing)"}`);
|
||||
console.log(`Available services: ${Object.keys(SERVICES).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log(`\n🔧 Phone CLI - ${command}\n`);
|
||||
// Validate command
|
||||
if (!command || !(command in COMMANDS)) {
|
||||
console.error(`❌ Unknown command: ${command || "(missing)"}`);
|
||||
console.log(`Available commands: ${Object.keys(COMMANDS).join(", ")}`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Parse service-specific commands
|
||||
const match = command.match(/^(ap|web)-(.+)$/);
|
||||
if (match) {
|
||||
const [, prefix, action] = match;
|
||||
const service = SERVICES[prefix as keyof typeof SERVICES];
|
||||
// Get systemd service name
|
||||
const serviceName = SERVICES[service as keyof typeof SERVICES];
|
||||
|
||||
switch (action) {
|
||||
// Execute command
|
||||
console.log(`\n🔧 Phone CLI - ${service} ${command}\n`);
|
||||
|
||||
const logCommand = (cmd: string) => {
|
||||
if (verbose) {
|
||||
console.log(`→ ${cmd}\n`);
|
||||
}
|
||||
};
|
||||
|
||||
switch (command) {
|
||||
case "status":
|
||||
console.log(`━━━ ${service}.service ━━━`);
|
||||
await $`systemctl status ${service}.service --no-pager -l`.nothrow();
|
||||
logCommand(`systemctl status ${serviceName}.service --no-pager -l`);
|
||||
await $`systemctl status ${serviceName}.service --no-pager -l`.nothrow();
|
||||
break;
|
||||
|
||||
case "logs":
|
||||
console.log(`📋 Recent logs (last 50 lines):\n`);
|
||||
await $`journalctl -u ${service}.service -n 50 --no-pager`.nothrow();
|
||||
logCommand(`journalctl -u ${serviceName}.service -n 50 --no-pager`);
|
||||
await $`journalctl -u ${serviceName}.service -n 50 --no-pager`.nothrow();
|
||||
break;
|
||||
|
||||
case "tail":
|
||||
console.log(`📡 Tailing logs (Ctrl+C to stop)...\n`);
|
||||
await $`journalctl -u ${service}.service -f --no-pager`.nothrow();
|
||||
logCommand(`journalctl -u ${serviceName}.service -f --no-pager`);
|
||||
await $`journalctl -u ${serviceName}.service -f --no-pager`.nothrow();
|
||||
break;
|
||||
|
||||
case "restart":
|
||||
console.log(`🔄 Restarting ${service}.service...\n`);
|
||||
await $`sudo systemctl restart ${service}.service`;
|
||||
console.log(`✓ ${service}.service restarted!`);
|
||||
console.log(`🔄 Restarting ${serviceName}.service...\n`);
|
||||
logCommand(`sudo systemctl restart ${serviceName}.service`);
|
||||
await $`sudo systemctl restart ${serviceName}.service`;
|
||||
console.log(`✓ ${serviceName}.service restarted!`);
|
||||
break;
|
||||
|
||||
case "stop":
|
||||
console.log(`🛑 Stopping ${service}.service...\n`);
|
||||
await $`sudo systemctl stop ${service}.service`;
|
||||
console.log(`✓ ${service}.service stopped!`);
|
||||
console.log(`🛑 Stopping ${serviceName}.service...\n`);
|
||||
logCommand(`sudo systemctl stop ${serviceName}.service`);
|
||||
await $`sudo systemctl stop ${serviceName}.service`;
|
||||
console.log(`✓ ${serviceName}.service stopped!`);
|
||||
break;
|
||||
|
||||
case "start":
|
||||
console.log(`▶️ Starting ${service}.service...\n`);
|
||||
await $`sudo systemctl start ${service}.service`;
|
||||
console.log(`✓ ${service}.service started!`);
|
||||
console.log(`▶️ Starting ${serviceName}.service...\n`);
|
||||
logCommand(`sudo systemctl start ${serviceName}.service`);
|
||||
await $`sudo systemctl start ${serviceName}.service`;
|
||||
console.log(`✓ ${serviceName}.service started!`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// All-services commands
|
||||
const allServices = Object.values(SERVICES);
|
||||
|
||||
switch (command) {
|
||||
case "status":
|
||||
for (const service of allServices) {
|
||||
console.log(`━━━ ${service}.service ━━━`);
|
||||
await $`systemctl status ${service}.service --no-pager -l`.nothrow();
|
||||
console.log("");
|
||||
}
|
||||
break;
|
||||
|
||||
case "logs":
|
||||
console.log("📋 Recent logs (last 50 lines):\n");
|
||||
const serviceFlags = allServices.map(s => `-u ${s}.service`).join(" ");
|
||||
await $`journalctl ${serviceFlags} -n 50 --no-pager`.nothrow();
|
||||
break;
|
||||
|
||||
case "tail":
|
||||
console.log("📡 Tailing logs (Ctrl+C to stop)...\n");
|
||||
const tailFlags = allServices.map(s => `-u ${s}.service`).join(" ");
|
||||
await $`journalctl ${tailFlags} -f --no-pager`.nothrow();
|
||||
break;
|
||||
|
||||
case "restart":
|
||||
console.log("🔄 Restarting services...\n");
|
||||
for (const service of allServices) {
|
||||
console.log(`Restarting ${service}.service...`);
|
||||
await $`sudo systemctl restart ${service}.service`;
|
||||
console.log(`✓ ${service}.service restarted`);
|
||||
}
|
||||
console.log("\n✓ All services restarted!");
|
||||
break;
|
||||
|
||||
case "stop":
|
||||
console.log("🛑 Stopping services...\n");
|
||||
for (const service of allServices) {
|
||||
console.log(`Stopping ${service}.service...`);
|
||||
await $`sudo systemctl stop ${service}.service`;
|
||||
console.log(`✓ ${service}.service stopped`);
|
||||
}
|
||||
console.log("\n✓ All services stopped!");
|
||||
break;
|
||||
|
||||
case "start":
|
||||
console.log("▶️ Starting services...\n");
|
||||
for (const service of allServices) {
|
||||
console.log(`Starting ${service}.service...`);
|
||||
await $`sudo systemctl start ${service}.service`;
|
||||
console.log(`✓ ${service}.service started`);
|
||||
}
|
||||
console.log("\n✓ All services started!");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
console.log("");
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
# Agent
|
||||
|
||||
A clean, reusable wrapper for ElevenLabs conversational AI WebSocket protocol. Uses Signal-based events and provides simple tool registration.
|
||||
A clean, reusable wrapper for ElevenLabs conversational AI WebSocket protocol. Uses events and provides simple tool registration.
|
||||
|
||||
## Basic Usage
|
||||
|
||||
```typescript
|
||||
import { Agent } from './pi/agent'
|
||||
import Buzz from './pi/buzz'
|
||||
import { Agent } from "./pi/agent"
|
||||
import Buzz from "./pi/buzz"
|
||||
|
||||
const agent = new Agent({
|
||||
agentId: process.env.ELEVEN_AGENT_ID!,
|
||||
|
|
@ -14,27 +14,24 @@ const agent = new Agent({
|
|||
tools: {
|
||||
search_web: async (args) => {
|
||||
return { results: [`Result for ${args.query}`] }
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
// Set up event handlers
|
||||
const player = await Buzz.player()
|
||||
let playback = player.playStream()
|
||||
|
||||
agent.events.connect((event) => {
|
||||
if (event.type === 'audio') {
|
||||
const audioBuffer = Buffer.from(event.audioBase64, 'base64')
|
||||
agent.events.on((event) => {
|
||||
if (event.type === "audio") {
|
||||
const audioBuffer = Buffer.from(event.audioBase64, "base64")
|
||||
if (!playback.isPlaying) playback = player.playStream()
|
||||
playback.write(audioBuffer)
|
||||
}
|
||||
else if (event.type === 'interruption') {
|
||||
} else if (event.type === "interruption") {
|
||||
playback.stop()
|
||||
}
|
||||
else if (event.type === 'user_transcript') {
|
||||
} else if (event.type === "user_transcript") {
|
||||
console.log(`User: ${event.transcript}`)
|
||||
}
|
||||
else if (event.type === 'agent_response') {
|
||||
} else if (event.type === "agent_response") {
|
||||
console.log(`Agent: ${event.response}`)
|
||||
}
|
||||
})
|
||||
|
|
@ -68,7 +65,7 @@ for await (const chunk of recording.stream()) {
|
|||
if (rms > vadThreshold) {
|
||||
// Speech detected! Start conversation
|
||||
agent = new Agent({ agentId, apiKey, tools })
|
||||
agent.events.connect(eventHandler)
|
||||
agent.events.on(eventHandler)
|
||||
await agent.start()
|
||||
|
||||
// Send buffered audio
|
||||
|
|
@ -112,7 +109,7 @@ new Agent({
|
|||
|
||||
### Properties
|
||||
|
||||
- `agent.events: Signal<AgentEvent>` - Connect to receive all events
|
||||
- `agent.events: Emitter<AgentEvent>` - Connect to receive all events
|
||||
- `agent.isConnected: boolean` - Current connection state
|
||||
- `agent.conversationId?: string` - Available after connected event
|
||||
|
||||
|
|
@ -121,11 +118,13 @@ new Agent({
|
|||
All events are emitted through `agent.events`:
|
||||
|
||||
### Connection
|
||||
|
||||
- `{ type: 'connected', conversationId, audioFormat }`
|
||||
- `{ type: 'disconnected' }`
|
||||
- `{ type: 'error', error }`
|
||||
|
||||
### Conversation
|
||||
|
||||
- `{ type: 'user_transcript', transcript }`
|
||||
- `{ type: 'agent_response', response }`
|
||||
- `{ type: 'agent_response_correction', original, corrected }`
|
||||
|
|
@ -134,11 +133,13 @@ All events are emitted through `agent.events`:
|
|||
- `{ type: 'interruption', eventId }`
|
||||
|
||||
### Tools
|
||||
|
||||
- `{ type: 'tool_call', name, args, callId }`
|
||||
- `{ type: 'tool_result', name, result, callId }`
|
||||
- `{ type: 'tool_error', name, error, callId }`
|
||||
|
||||
### Optional
|
||||
|
||||
- `{ type: 'vad_score', score }`
|
||||
- `{ type: 'ping', eventId, pingMs }`
|
||||
|
||||
|
|
@ -146,7 +147,7 @@ All events are emitted through `agent.events`:
|
|||
|
||||
- **Generic**: Not tied to phone systems, works in any context
|
||||
- **Flexible audio**: You control when to send audio, Agent just handles WebSocket
|
||||
- **Event-driven**: All communication through Signal events, no throws
|
||||
- **Event-driven**: All communication through events, no throws
|
||||
- **Simple tools**: Just pass a function map to constructor
|
||||
- **Automatic buffering**: Sends buffered audio when connection opens
|
||||
- **Automatic chunking**: Handles 8000-byte chunking internally
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Signal } from "../utils/signal"
|
||||
import { Emitter } from "../utils/emitter"
|
||||
import type { AgentConfig, AgentEvent } from "./types"
|
||||
|
||||
type AgentState = "disconnected" | "connecting" | "connected"
|
||||
|
|
@ -11,7 +11,7 @@ export class Agent {
|
|||
#chunkBuffer = new Uint8Array(0)
|
||||
#chunkSize = 8000
|
||||
|
||||
public readonly events = new Signal<AgentEvent>()
|
||||
public readonly events = new Emitter<AgentEvent>()
|
||||
public conversationId?: string
|
||||
|
||||
constructor(config: AgentConfig) {
|
||||
|
|
|
|||
17
src/phone.ts
17
src/phone.ts
|
|
@ -46,7 +46,7 @@ export const runPhone = async (agentId: string, agentKey: string) => {
|
|||
using rotaryInUse = gpio.input(22, { pull: "up", debounce: 3 })
|
||||
using rotaryNumber = gpio.input(23, { pull: "up", debounce: 3 })
|
||||
|
||||
await Buzz.setVolume(0.2)
|
||||
await Buzz.setVolume(0.3)
|
||||
log(`📞 Phone is ${hook.value ? "off hook" : "on hook"}`)
|
||||
|
||||
playStartRing(ringer)
|
||||
|
|
@ -94,7 +94,7 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
|||
const baresipConfig = join(import.meta.dir, "..", "baresip")
|
||||
const baresip = new Baresip(["/usr/bin/baresip", "-v", "-f", baresipConfig])
|
||||
|
||||
baresip.registrationSuccess.connect(async () => {
|
||||
baresip.registrationSuccess.on(async () => {
|
||||
log("🐻 server connected")
|
||||
if (hook.value === 0) {
|
||||
phoneService.send({ type: "initialized" })
|
||||
|
|
@ -103,17 +103,17 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
|||
}
|
||||
})
|
||||
|
||||
baresip.callReceived.connect(({ contact }) => {
|
||||
baresip.callReceived.on(({ contact }) => {
|
||||
log(`🐻 incoming call from ${contact}`)
|
||||
phoneService.send({ type: "incoming-call", from: contact })
|
||||
})
|
||||
|
||||
baresip.callEstablished.connect(({ contact }) => {
|
||||
baresip.callEstablished.on(({ contact }) => {
|
||||
log(`🐻 call established with ${contact}`)
|
||||
phoneService.send({ type: "answered" })
|
||||
})
|
||||
|
||||
baresip.hungUp.connect(() => {
|
||||
baresip.hungUp.on(() => {
|
||||
log("🐻 call hung up")
|
||||
phoneService.send({ type: "remote-hang-up" })
|
||||
})
|
||||
|
|
@ -123,7 +123,7 @@ const startBaresip = async (phoneService: PhoneService, hook: GPIO.Input, ringer
|
|||
phoneService.send({ type: "error", message: error.message })
|
||||
})
|
||||
|
||||
baresip.error.connect(async ({ message }) => {
|
||||
baresip.error.on(async ({ message }) => {
|
||||
log.error("🐻 error:", message)
|
||||
phoneService.send({ type: "error", message })
|
||||
for (let i = 0; i < 4; i++) {
|
||||
|
|
@ -200,7 +200,7 @@ const startListening = (service: Service<typeof phoneMachine>, agent: Agent) =>
|
|||
|
||||
let preConnectionBuffer: Uint8Array[] = []
|
||||
|
||||
agent.events.connect(async (event) => {
|
||||
agent.events.on(async (event) => {
|
||||
if (event.type === "disconnected") abortAgent.abort()
|
||||
})
|
||||
|
||||
|
|
@ -250,7 +250,7 @@ const handleAgentEvents = (
|
|||
) => {
|
||||
const waitingIndicator = new WaitingSounds(player)
|
||||
|
||||
agent.events.connect(async (event) => {
|
||||
agent.events.on(async (event) => {
|
||||
switch (event.type) {
|
||||
case "connected":
|
||||
log("🤖 Connected to AI agent\n")
|
||||
|
|
@ -272,6 +272,7 @@ const handleAgentEvents = (
|
|||
|
||||
case "interruption":
|
||||
log("🤖 User interrupted")
|
||||
await waitingIndicator.stop()
|
||||
streamPlayback?.stop()
|
||||
streamPlayback = player.playStream() // Reset playback stream
|
||||
break
|
||||
|
|
|
|||
20
src/sip.ts
20
src/sip.ts
|
|
@ -1,15 +1,15 @@
|
|||
import log from "./utils/log.ts"
|
||||
import { Signal } from "./utils/signal.ts"
|
||||
import { Emitter } from "./utils/emitter.ts"
|
||||
import { processStdout, processStderr } from "./utils/stdio.ts"
|
||||
|
||||
export class Baresip {
|
||||
baresipArgs: string[]
|
||||
process?: Bun.PipedSubprocess
|
||||
callEstablished = new Signal<{ contact: string }>()
|
||||
callReceived = new Signal<{ contact: string }>()
|
||||
hungUp = new Signal()
|
||||
error = new Signal<{ message: string }>()
|
||||
registrationSuccess = new Signal()
|
||||
callEstablished = new Emitter<{ contact: string }>()
|
||||
callReceived = new Emitter<{ contact: string }>()
|
||||
hungUp = new Emitter()
|
||||
error = new Emitter<{ message: string }>()
|
||||
registrationSuccess = new Emitter()
|
||||
|
||||
constructor(baresipArgs: string[]) {
|
||||
this.baresipArgs = baresipArgs
|
||||
|
|
@ -48,10 +48,10 @@ export class Baresip {
|
|||
}
|
||||
|
||||
disconnectAll() {
|
||||
this.callEstablished.disconnect()
|
||||
this.callReceived.disconnect()
|
||||
this.hungUp.disconnect()
|
||||
this.registrationSuccess.disconnect()
|
||||
this.callEstablished.removeAllListeners()
|
||||
this.callReceived.removeAllListeners()
|
||||
this.hungUp.removeAllListeners()
|
||||
this.registrationSuccess.removeAllListeners()
|
||||
}
|
||||
|
||||
kill() {
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ const runPhoneSystem = async (agentId: string, apiKey: string) => {
|
|||
const waitingIndicator = new WaitingSounds(player)
|
||||
|
||||
// Set up agent event listeners
|
||||
agent.events.connect(async (event) => {
|
||||
agent.events.on(async (event) => {
|
||||
switch (event.type) {
|
||||
case "connected":
|
||||
console.log("✅ Connected to AI agent\n")
|
||||
|
|
@ -162,7 +162,7 @@ if (!apiKey) {
|
|||
|
||||
if (!agentId) {
|
||||
console.error(
|
||||
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required"
|
||||
"❌ Error: ELEVEN_AGENT_ID environELEVEN_AGENT_ID=agent_5601k4taw2cvfjzrz6snxpgeh7x8 ELEVEN_API_KEY=sk_0313740f112c5992cb62ed96c974ab19b5916f1ea172471fment variable is required",
|
||||
)
|
||||
console.error(" Create an agent at https://elevenlabs.io/app/conversational-ai")
|
||||
process.exit(1)
|
||||
|
|
|
|||
42
src/utils/emitter.ts
Normal file
42
src/utils/emitter.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* How to use Emitter:
|
||||
*
|
||||
* Create an emitter:
|
||||
* const chat = new Emitter<{ username: string, message: string }>()
|
||||
*
|
||||
* Listen to events:
|
||||
* const off = chat.on((data) => {
|
||||
* const {username, message} = data;
|
||||
* console.log(`${username} said "${message}"`);
|
||||
* })
|
||||
*
|
||||
* Emit an event:
|
||||
* chat.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
||||
*
|
||||
* Remove a specific listener:
|
||||
* off(); // The off function is returned when you add a listener
|
||||
*
|
||||
* Remove all listeners:
|
||||
* chat.removeAllListeners()
|
||||
*/
|
||||
export class Emitter<T = void> {
|
||||
private listeners: Array<(data: T) => void> = []
|
||||
|
||||
on(listener: (data: T) => void) {
|
||||
this.listeners.push(listener)
|
||||
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
|
||||
emit(data: T) {
|
||||
for (const listener of this.listeners) {
|
||||
listener(data)
|
||||
}
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
this.listeners = []
|
||||
}
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
let showDebug = true
|
||||
let showDebug = process.env.DEBUG ?? false
|
||||
let showInfo = true
|
||||
|
||||
export function setLogLevel(level: "debug" | "info" | "error") {
|
||||
|
|
|
|||
|
|
@ -1,96 +0,0 @@
|
|||
/**
|
||||
* How to use a Signal:
|
||||
*
|
||||
* Create a signal:
|
||||
* const chatSignal = new Signal<{ username: string, message: string }>()
|
||||
*
|
||||
* Connect to the signal:
|
||||
* const disconnect = chatSignal.connect((data) => {
|
||||
* const {username, message} = data;
|
||||
* console.log(`${username} said "${message}"`);
|
||||
* })
|
||||
*
|
||||
* Emit a signal:
|
||||
* chatSignal.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
||||
*
|
||||
*
|
||||
* Disconnect a single listener:
|
||||
* disconnect(); // The disconnect function is returned when you connect to a signal
|
||||
*
|
||||
* Disconnect all listeners:
|
||||
* chatSignal.disconnect()
|
||||
*/
|
||||
|
||||
export class Signal<T extends object | void> {
|
||||
private listeners: Array<(data: T) => void> = []
|
||||
|
||||
connect(listenerOrSignal: Signal<T> | ((data: T) => void)) {
|
||||
let listener: (data: T) => void
|
||||
|
||||
// If it is a signal, forward the data to the signal
|
||||
if (listenerOrSignal instanceof Signal) {
|
||||
listener = (data: T) => listenerOrSignal.emit(data)
|
||||
} else {
|
||||
listener = listenerOrSignal
|
||||
}
|
||||
|
||||
this.listeners.push(listener)
|
||||
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
|
||||
emit(data: T) {
|
||||
for (const listener of this.listeners) {
|
||||
listener(data)
|
||||
}
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.listeners = []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* How to use Emitter:
|
||||
*
|
||||
* Create an emitter:
|
||||
* const chat = new Emitter<{ username: string, message: string }>()
|
||||
*
|
||||
* Listen to events:
|
||||
* const off = chat.on((data) => {
|
||||
* const {username, message} = data;
|
||||
* console.log(`${username} said "${message}"`);
|
||||
* })
|
||||
*
|
||||
* Emit an event:
|
||||
* chat.emit({ username: "Chad", message: "Hey everyone, how's it going?" });
|
||||
*
|
||||
* Remove a specific listener:
|
||||
* off(); // The off function is returned when you add a listener
|
||||
*
|
||||
* Remove all listeners:
|
||||
* chat.removeAllListeners()
|
||||
*/
|
||||
export class Emitter<T = void> {
|
||||
private listeners: Array<(data: T) => void> = []
|
||||
|
||||
on(listener: (data: T) => void) {
|
||||
this.listeners.push(listener)
|
||||
|
||||
return () => {
|
||||
this.listeners = this.listeners.filter((l) => l !== listener)
|
||||
}
|
||||
}
|
||||
|
||||
emit(data: T) {
|
||||
for (const listener of this.listeners) {
|
||||
listener(data)
|
||||
}
|
||||
}
|
||||
|
||||
removeAllListeners() {
|
||||
this.listeners = []
|
||||
}
|
||||
}
|
||||
|
|
@ -6,16 +6,22 @@ import { log } from "console"
|
|||
export class WaitingSounds {
|
||||
typingPlayback?: Buzz.Playback
|
||||
speakingPlayback?: Buzz.Playback
|
||||
playing = false
|
||||
|
||||
constructor(private player: Buzz.Player) {}
|
||||
|
||||
async start(operatorStream: Buzz.StreamingPlayback) {
|
||||
if (this.typingPlayback) return // Already playing
|
||||
if (this.playing) return // Already playing
|
||||
|
||||
this.playing = true
|
||||
this.#startTypingSounds()
|
||||
this.#startSpeakingSounds(operatorStream)
|
||||
}
|
||||
|
||||
get isPlaying() {
|
||||
return this.playing
|
||||
}
|
||||
|
||||
async #startTypingSounds() {
|
||||
return new Promise<void>(async (resolve) => {
|
||||
do {
|
||||
|
|
@ -29,7 +35,7 @@ export class WaitingSounds {
|
|||
const typingSound = getSound(dir)
|
||||
this.typingPlayback = await this.player.play(typingSound)
|
||||
await this.typingPlayback.finished()
|
||||
} while (this.typingPlayback)
|
||||
} while (this.isPlaying)
|
||||
|
||||
resolve()
|
||||
})
|
||||
|
|
@ -64,20 +70,17 @@ export class WaitingSounds {
|
|||
this.speakingPlayback = await this.player.play(speakingSound)
|
||||
playedSounds.add(speakingSound)
|
||||
await this.speakingPlayback.finished()
|
||||
} while (this.typingPlayback)
|
||||
} while (this.isPlaying)
|
||||
resolve()
|
||||
})
|
||||
}
|
||||
|
||||
async stop() {
|
||||
log(`🛑 Stopping waiting sounds. Has typingPlayback: ${!!this.typingPlayback}`)
|
||||
if (!this.typingPlayback) return
|
||||
log(`🛑 Stopping waiting sounds. Playing? ${this.playing}`)
|
||||
if (!this.playing) return
|
||||
this.playing = false
|
||||
|
||||
// Quicky undefine this to stop the loops
|
||||
const typingPlayback = this.typingPlayback
|
||||
this.typingPlayback = undefined
|
||||
|
||||
await Promise.all([typingPlayback.stop(), this.speakingPlayback?.finished()])
|
||||
await Promise.all([this.typingPlayback?.stop(), this.speakingPlayback?.finished()])
|
||||
log("🛑 Waiting sounds stopped")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user