FireTV Voice: A Private, On-Device Voice Remote for My Fire TV

AI;DR (AI Didn’t Read)

This part is for you, the human. Everything after it is written for your coding agent.

Why are we waiting on Alexa (or whatever assistant shipped in the box), why not just build the thing you actually want? mostly to make my own life a little smoother.

This one is a voice remote for my Fire TV that lives on my Mac.

If you want one too, hand this page to Claude Code (or your agent of choice) and tell it to build it for your setup.

The code, scripts and tests are on GitHub: 97harsh/firetv-voice.


For agents: overview

Goal: an always-listening, owner-only, fully local voice remote for an Amazon Fire TV, running on an Apple Silicon Mac on the same network. No push-to-talk. Target: end of speech to command sent in under ~500 ms.

Two problems define the design: the TV talks constantly, and so do the people in the room. Every utterance therefore has to pass a speaker check (is this the owner?) and an addressee check (is this said to the TV?) before it acts.

Reference implementation: 97harsh/firetv-voice — Python engine, two small Swift apps, launchd agents, ~2,300 lines total. Unit tests for the parser and addressee logic run with pytest; scripts/ has the live TV test and a standalone hid remote demo.

Architecture

mic
 └─ FireTVVoice.app (mic, 16 kHz)
     └─ Silero VAD
         ├─ voice check (CPU)
         │   TitaNet + ERes2Net
         │   + trained head
         └─ whisper.cpp (GPU)
             small.en, Metal
     └─ rule parser
         └─ persistent ADB link
             ├─ hid (virtual remote)
             └─ am start / media
 └─ FireTVVoiceUI.app
     glow + pop-up + menu bar

Components:

  1. Mic host (Swift .app). Owns microphone permission and streams 16 kHz mono audio to the Python engine, which it spawns as a child.
  2. VAD. Silero VAD. Defaults: threshold 0.5, min silence 0.25 s, min speech 0.2 s, max speech 8 s.
  3. Speaker check. sherpa-onnx with NeMo TitaNet-small and 3D-Speaker ERes2Net, plus a per-user logistic-regression head.
  4. Speech-to-text. whisper.cpp small.en (q5_1) on Metal, kept warm in a local whisper-server child process (warm-up request at start so the first command doesn’t pay shader compilation).
  5. Intent parser. Regex rules. No LLM in the hot path.
  6. TV link. Persistent ADB over the network: one plain shell and one hid virtual remote.
  7. UI (Swift .app). Menu bar icon, screen-edge glow and pop-ups, driven by JSON events over localhost UDP. Needs no permissions.
  8. CLI. ftv status | start | stop | enroll | observe | live | say <text> | log | latency | wrong | reload.

Per-utterance pipeline

  1. VAD closes a segment after 0.25 s of silence.
  2. Submit STT (GPU) and the speaker check (CPU) in parallel. If the speaker check fails, discard the transcript and add the embedding to the impostor cohort.
  3. Normalise the text: strip filler (“please”, “can you”, “hey”) and detect the optional wake word (“TV”, “hey TV”, “fire TV”…). A bare “TV” arms the next phrase for 5 s.
  4. Parse. The whole phrase must match a command; partial matches inside a longer sentence are ignored.
  5. Apply the addressee rules (below).
  6. In observe mode, show “Would: …”. In live mode, send to the TV and report latency.
  7. Log every decision to a JSONL event log with timings and context.

Controlling a Fire TV over ADB: what works and what doesn’t

Tested on Fire OS 8.

Speaker verification (owner-only)

Addressee rules (TV or a person?)

Intents to support

UX

macOS gotchas

Measured latency

M1 MacBook Air, end of speech to command sent, dry runs on recorded commands:

stagetypical
end-of-speech detection (VAD silence window)~255 ms
your-voice check (runs in parallel with STT)40–125 ms
whisper.cpp small.en (Metal)180–250 ms
intent parsing< 1 ms
total, end of speech → command sent440–510 ms
ADB delivery to TV (persistent link): key~10–20 ms
ADB delivery: am start / media command~30–60 ms

Live, on-TV latency in a real room has not been measured yet. The TV also needs its own time to draw the result (opening an app is a cold or warm start on the TV’s slow CPU, same as with Alexa).

Known limitations

Status and next steps

  1. Enroll the owner’s voice (natural talk, ~2 minutes).
  2. Live test on the TV: search deep links, sleep/wake, and real end-to-end latency in the room.
  3. Switch from observe to live, then collect labelled data for a learned addressee model.

I’ll update this post with real-room numbers once that’s done.