Skip to content

cducote/state-machine

Repository files navigation

AnimalAI Voice-Flow Application

A modular, voice-driven state machine application built with XState v5 + React + TypeScript.

This project demonstrates a production-ready architecture for voice-controlled applications, using the state machine as the application "spine" with clean separation of concerns.

Architecture Overview

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                          UI Layer                               β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ VoiceConsoleβ”‚ β”‚ StatePanel  β”‚ β”‚ LogsPanel   β”‚ ...           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚         β”‚               β”‚               β”‚                       β”‚
β”‚         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                       β”‚
β”‚                         β–Ό                                       β”‚
β”‚                    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”                                  β”‚
β”‚                    β”‚ AppShellβ”‚  (thin client, no business logic)β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό events
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                      Voice Layer                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚intentRouter β”‚ β”‚responseBank β”‚ β”‚   aliases   β”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β”‚         β”‚                                                       β”‚
β”‚         β–Ό machine events                                        β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    State Machine                                β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”‚
β”‚  β”‚              animalMachine.ts                    β”‚           β”‚
β”‚  β”‚  (pure state machine - the application spine)    β”‚           β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚
β”‚                         β”‚                                       β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚ types.ts β”‚ β”‚    guards.ts      β”‚ β”‚actions.tsβ”‚               β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
                          β”‚
                          β–Ό invokes
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                       Actors                                    β”‚
β”‚  β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”               β”‚
β”‚  β”‚playbackActorβ”‚ β”‚grooveSearch β”‚ β”‚narrationActorβ”‚              β”‚
β”‚  β”‚  (timers)   β”‚ β”‚  (search)   β”‚ β”‚ (LLM/phi3)  β”‚              β”‚
β”‚  β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Folder Structure

src/
β”œβ”€β”€ machine/                 # Core state machine (pure, deterministic)
β”‚   β”œβ”€β”€ animalMachine.ts     # The spine - machine definition
β”‚   β”œβ”€β”€ types.ts             # Context, events, and type definitions
β”‚   β”œβ”€β”€ guards.ts            # Pure guard functions
β”‚   └── actions.ts           # Action implementations
β”‚
β”œβ”€β”€ actors/                  # Side effects, async, cancellable
β”‚   β”œβ”€β”€ playbackActor.ts     # Timer-based playback simulation
β”‚   β”œβ”€β”€ grooveSearchActor.ts # Deterministic groove search
β”‚   └── narrationActor.ts    # LLM-powered narration (phi3:mini)
β”‚
β”œβ”€β”€ voice/                   # Voice/utterance handling
β”‚   β”œβ”€β”€ intentRouter.ts      # Utterance β†’ intent β†’ events
β”‚   β”œβ”€β”€ responseBank.ts      # Fallback phrase banks
β”‚   └── aliases.ts           # Selection phrase aliases (A/B/C)
β”‚
β”œβ”€β”€ runtime/                 # Machine runtime utilities
β”‚   β”œβ”€β”€ interpret.ts         # Actor creation and wiring
β”‚   └── logger.ts            # Structured logging
β”‚
β”œβ”€β”€ ui/                      # React UI (thin client)
β”‚   β”œβ”€β”€ AppShell.tsx         # Main application shell
β”‚   └── panels/              # UI panels
β”‚       β”œβ”€β”€ StatePanel.tsx
β”‚       β”œβ”€β”€ ContextPanel.tsx
β”‚       β”œβ”€β”€ LogsPanel.tsx
β”‚       β”œβ”€β”€ PlaybackPanel.tsx
β”‚       β”œβ”€β”€ VoiceConsole.tsx
β”‚       └── AnimalSaysPanel.tsx
β”‚
β”œβ”€β”€ util/                    # Shared utilities
β”‚   β”œβ”€β”€ time.ts              # Time formatting, delays
β”‚   └── random.ts            # Random selection helpers
β”‚
β”œβ”€β”€ App.tsx                  # Entry point
└── App.css                  # Styles

State Machine Hierarchy

idleArmed (powered on, not listening)
    ↓ WAKE
idleListening (awaiting command)
    ↓ NEW_SESSION
sessionActive (parent state)
    β”œβ”€β”€ askingVibe (prompts user)
    β”œβ”€β”€ waitingForRequest (expects groove description)
    β”œβ”€β”€ lookingUpGrooves (async search)
    β”œβ”€β”€ announceAudition (brief pause)
    β”œβ”€β”€ auditioning (Aβ†’Bβ†’C, 4 bars each)
    β”œβ”€β”€ unsureSelection (retry/fallback)
    └── jamming (loop selected groove)
error (recoverable)

Quick Start

# Install dependencies
npm install

# Start development server
npm run dev

# Build for production
npm run build

Open http://localhost:5173

Voice Implementation (Phased)

Phase 1: Voice Line Buttons (Current)

  • Buttons simulate spoken phrases
  • VoiceConsole sends VOICE_UTTERANCE({ text })
  • intentRouter maps utterances to machine events
  • No real speech recognition required

Phase 2: Browser SpeechRecognition (Future)

  • Add src/adapters/speechRecognitionAdapter.ts
  • Use Web Speech API
  • Same intent routing, just different input source

Phase 3: TTS (Future)

  • Add src/adapters/ttsAdapter.ts
  • Use SpeechSynthesis or macOS say
  • Speak context.animalLine on change

NarrationActor (LLM via Ollama)

The only LLM usage is in narrationActor.ts for generating Animal's spoken lines.

Setup

# Install Ollama (macOS)
brew install ollama

# Start Ollama server
ollama serve

# Pull the model
ollama pull phi3:mini

Enable

Create a .env file:

VITE_OLLAMA_BASE_URL=http://localhost:11434
VITE_NARRATION_ENABLED=true

Features

  • Strict timeout: 600ms, then fallback to response bank
  • JSON output: Forces { "line": "..." } format
  • Validation: Invalid responses trigger fallback
  • Caching: Repeated prompts return cached responses
  • Graceful degradation: Works without Ollama

Use Cases

  • Wake greeting
  • Ask vibe line
  • Audition intro
  • Unsure prompt
  • Jamming confirmation
  • Optional candidate callouts

Try This Script

Follow this exact click sequence to test the full workflow:

1. Wake β†’ New Session β†’ Groove Request β†’ Audition

  1. Click "animal" (wake word)

    • State: idleArmed β†’ idleListening
    • Animal greets you
  2. Click "start a new session"

    • State: idleListening β†’ sessionActive.waitingForRequest
    • Animal asks for vibe
  3. Click "play a chill rock beat at 112 BPM..."

    • State: waitingForRequest β†’ lookingUpGrooves β†’ auditioning
    • Watch grooves A, B, C play

2. Interrupt and Select

  1. While auditioning, click "first" (or "A")
    • State: auditioning β†’ jamming
    • Selected groove loops

3. Barge In

  1. Click BARGE_IN
    • State: jamming β†’ waitingForRequest
    • Loop stops, session preserved

4. Fallback Path

  1. Start a new audition
  2. Click "gibberish / unclear" twice
    • Fallback buttons appear
  3. Click a fallback button to select

Adding New Voice Commands

1. Add phrases to intentRouter.ts

const MY_COMMAND_PHRASES = [
  'do something',
  'make it happen',
];

2. Add intent type

export type IntentType = 
  | ...existing...
  | 'my_command';

3. Add state validity

const INTENT_VALID_STATES = {
  ...existing...,
  my_command: ['sessionActive.waitingForRequest'],
};

4. Handle in AppShell

case 'my_command':
  send({ type: 'MY_COMMAND' });
  break;

5. Add event to machine

Add the event type and transitions in animalMachine.ts.


Edge Cases Covered

Scenario Behavior
"new session" before wake Implicitly wakes and starts session
Groove request in wrong state Logged as blocked
"A"/"first" during audition Interrupts and starts jamming
2 unclear utterances Enables fallback buttons
STOP from any state Resets to idleArmed
BARGE_IN during jamming Pauses without losing session
200 bars in jamming Auto-stops (safety limit)

Tech Stack

  • Vite - Build tool
  • React 18 - UI
  • TypeScript - Type safety
  • XState v5 - State machine library
  • @xstate/react - React bindings
  • Ollama + phi3:mini - LLM for narration (optional)

Design Principles

  1. Machine as Spine: The state machine is the source of truth
  2. Pure Core: Machine logic is deterministic and testable
  3. Actors for Effects: All side effects live in actors
  4. Thin UI: No business logic in components
  5. One LLM Actor: Narration only; everything else deterministic
  6. Graceful Fallbacks: Works without external dependencies

License

MIT

About

state machine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors