Files
gnome-jarvis/daemon/voice-state.js
T
snxraven 2f8f369f24
Rolling release / release (push) Successful in 3m26s
Allow typed prompts from armed state
2026-09-11 17:36:20 -04:00

57 lines
2.0 KiB
JavaScript

const STATES = ['ARMED', 'LISTENING', 'THINKING', 'SPEAKING', 'SLEEPING'];
const COMMANDS = new Map([
['cancel', 'cancel'], ['stop', 'cancel'], ['never mind', 'cancel'],
['hands off', 'cancel'], ['stop clicking', 'cancel'], ["that's enough", 'cancel'],
['go to sleep', 'sleep'], ['privacy mode', 'sleep'],
]);
export class VoiceStateMachine {
constructor({ now = () => Date.now(), idleMs = 30 * 60 * 1000 } = {}) {
this.now = now;
this.idleMs = idleMs;
this.state = 'ARMED';
this.lastActivity = now();
}
transition(next, reason = 'unspecified') {
if (!STATES.includes(next)) throw new Error(`unknown Jarvis state: ${next}`);
const allowed = {
ARMED: ['LISTENING', 'SLEEPING'], LISTENING: ['THINKING', 'ARMED', 'SLEEPING'],
THINKING: ['SPEAKING', 'ARMED', 'LISTENING'], SPEAKING: ['LISTENING', 'ARMED'],
SLEEPING: ['LISTENING', 'ARMED'],
};
if (next !== this.state && !allowed[this.state].includes(next)) {
throw new Error(`invalid state transition ${this.state} -> ${next}`);
}
const previous = this.state;
this.state = next;
this.lastActivity = this.now();
return { previous, state: next, reason };
}
wake() { return this.transition('LISTENING', 'wake'); }
utterance() { return this.transition('THINKING', 'utterance'); }
typedUtterance() {
if (this.state === 'ARMED' || this.state === 'SLEEPING') this.wake();
return this.utterance();
}
speak() { return this.transition('SPEAKING', 'reply'); }
finishSpeaking() { return this.transition('LISTENING', 'playback-finished'); }
cancel() { return this.transition('ARMED', 'cancel'); }
sleep() { return this.transition('SLEEPING', 'idle-or-privacy'); }
command(text) {
const command = COMMANDS.get(String(text || '').trim().toLowerCase());
if (command === 'cancel') this.cancel();
if (command === 'sleep') this.sleep();
return command || null;
}
expireIdle() {
if (this.state !== 'SLEEPING' && this.now() - this.lastActivity >= this.idleMs) this.sleep();
return this.state;
}
}
export { STATES };