Add Pending_Query class and voice interaction improvements
- lib/pending-query.mjs: new state machine for query accumulation wake word, silence timer, send/cancel/pause/resume, instant dispatch, mode toggle (always listen / stop listening), mode query - query-demo.mjs: refactored to use Pending_Query; wake word on by default with silence timer; chimes for dispatch/working/cancel/activate - tts-server.mjs: track last_speak_at, expose /activity endpoint, chime playback via Python queue (soundfile + librosa), preload on startup - chatterbox-server.py: chime and preload commands via stdin protocol - lib/chatterbox-tts.mjs: play_chime and preload_chime methods - test-chime.mjs: simple chime test script - voices.yaml: configured ready/cancel/working/dispatch chimes - CLEANUP-PLAN.md: updated with current state, command vocabulary, future plans Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,6 @@
|
||||
import * as http from 'node:http'
|
||||
import * as fs from 'node:fs'
|
||||
import * as path from 'node:path'
|
||||
import { spawn } from 'node:child_process'
|
||||
import yaml from 'js-yaml'
|
||||
import { Chatterbox_Tts } from './lib/chatterbox-tts.mjs'
|
||||
|
||||
@@ -38,6 +37,7 @@ function reload_config() {
|
||||
|
||||
let { voices, chimes } = reload_config()
|
||||
let current_voice = null // name of active voice, or null
|
||||
let last_speak_at = 0
|
||||
|
||||
// --- TTS setup ---
|
||||
const tts = new Chatterbox_Tts()
|
||||
@@ -45,6 +45,16 @@ process.stderr.write('[tts-server] starting chatterbox...\n')
|
||||
await tts.init()
|
||||
process.stderr.write('[tts-server] chatterbox ready\n')
|
||||
|
||||
// Preload all configured chimes so first play has no decode latency
|
||||
{
|
||||
const { chimes: configured_chimes } = reload_config()
|
||||
for (const [name, file] of Object.entries(configured_chimes)) {
|
||||
tts.preload_chime(file).catch(err =>
|
||||
process.stderr.write(`[tts-server] preload failed for chime '${name}': ${err.message}\n`)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Serialize all speak requests through a promise chain
|
||||
let queue = Promise.resolve()
|
||||
|
||||
@@ -72,19 +82,16 @@ function send(res, status, body) {
|
||||
res.end(payload)
|
||||
}
|
||||
|
||||
function play_file(file_path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const player = spawn('pacat', ['--playback', file_path])
|
||||
player.on('close', code => code === 0 ? resolve() : reject(new Error(`pacat exited ${code}`)))
|
||||
player.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
const server = http.createServer(async (req, res) => {
|
||||
if (req.method === 'GET' && req.url === '/health') {
|
||||
return send(res, 200, { ok: true })
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/activity') {
|
||||
return send(res, 200, { last_speak_at })
|
||||
}
|
||||
|
||||
if (req.method === 'GET' && req.url === '/voices') {
|
||||
({ voices, chimes } = reload_config())
|
||||
const list = Object.entries(voices).map(([name, v]) => ({
|
||||
@@ -120,7 +127,7 @@ const server = http.createServer(async (req, res) => {
|
||||
const file = chimes[name] ?? null
|
||||
if (!file) return send(res, 404, { error: `chime not found: ${name}` })
|
||||
try {
|
||||
await enqueue(() => play_file(file))
|
||||
await enqueue(() => tts.play_chime(file))
|
||||
return send(res, 200, { ok: true })
|
||||
} catch (err) {
|
||||
return send(res, 500, { error: err.message })
|
||||
@@ -139,6 +146,7 @@ const server = http.createServer(async (req, res) => {
|
||||
if (!text) {
|
||||
return send(res, 400, { error: 'text required' })
|
||||
}
|
||||
last_speak_at = Date.now()
|
||||
|
||||
// Inject current voice as default audio_prompt if none provided
|
||||
if (!opts.audio_prompt && current_voice && voices[current_voice]) {
|
||||
|
||||
Reference in New Issue
Block a user