Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion docs/specs/0011-excita-wake-word-ops.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

Draft. Excita is Conduit's **wake-word service**: one process that both **runs wake-word detection** on live audio and provides the **ops plane** (labelling, debugging, training, configuring) that makes the models it runs better over time. Ops and detection live together on purpose — they share the engine adapters, they share the model store, and unifying them is what closes the data loop without an operator shipping files by scp.

> **Extended by [constructorfleet/conduit#213](https://github.com/constructorfleet/conduit/issues/213)** (Excita: microWakeWord + nanoWakeWord engines — capability contract, model import, deploy targets). Anchored by [ADR-0020](../adr/0020-wake-engine-adapters-are-partial.md), [ADR-0021](../adr/0021-filesystem-imported-models-are-read-only-in-ui.md), [ADR-0022](../adr/0022-phrase-is-engine-agnostic.md), [ADR-0023](../adr/0023-engine-capability-gaps-return-501.md); where the issue speaks, it supersedes the sketches below.

Other runtimes still exist (openWakeWord baked into a satellite, microWakeWord on an ESPHome device, `crates/conduit-wake` in Conduit itself). Excita is one of them; it is *also* the tool that trains their models and, via a shared engine-agnostic package format, can publish updates to them.

Anchors: [0005](0005-link-protocol.md) (link protocol), [0007](0007-excita-wake-events-side-channel.md) (`excita.wake-events` — Excita **is** the sender when it is the detector), [0010](0010-linked-service-lifecycle-and-dev.md) (linked-service lifecycle), [0004](0004-embedded-service-visual-consistency.md) (embedded panel visual consistency). Reference implementation shape: `services/instrumenta/`.
Expand Down Expand Up @@ -255,4 +257,4 @@ Follow-up PRs (each its own review):

- **Multi-operator labelling.** The schema supports `(clip, labeller)` but the UI is single-operator for v1. Do we need reconciliation UX when two operators disagree? Deferred until a second operator exists.
- **Clip retention beyond the delete window.** Legal-hold on a clip an operator wants to keep forever? Add a `pinned` boolean on `clip` when this comes up.
- **Training compute.** In-process is fine for openWakeWord on a laptop. microWakeWord's TF training will not be. Escape hatch is `EXCITA_TRAIN_WORKER_URL` — punt to an external worker if set — but not built until asked.
- **Training compute.** In-process is fine for openWakeWord on a laptop. microWakeWord's TF training will not be. Escape hatch is `EXCITA_TRAIN_WORKER_URL` — punt to an external worker if set — but not built until asked. **[#213](https://github.com/constructorfleet/conduit/issues/213)** lands µWW / nanoWakeWord with `train` → `NotSupportedError` pointing at that variable; the training worker protocol (queue semantics, artifact upload, credentials, cancellation) remains its own future spec.
6 changes: 5 additions & 1 deletion services/excita/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@ RUN pip install --no-cache-dir -r requirements.txt \
COPY services/excita ./excita

ENV EXCITA_DATA_DIR=/data \
EXCITA_WAKE_MODELS_DIR=/wake-models
EXCITA_WAKE_MODELS_DIR=/wake-models \
EXCITA_MODEL_IMPORT_DIR=/model-import
# `/data` holds SQLite + clips (write-heavy); `/wake-models` holds the two
# shared openWakeWord ONNX files + any per-phrase classifier — read-only in
# production, populated by `scripts/fetch-wake-models.sh` or a bind mount.
# Separate volume so an image update doesn't rewrite fetched models.
# `/model-import` is the pre-trained model drop point (spec 0013): scanned
# on boot and on SIGHUP, the volume is the source of truth for anything
# mounted there (ADR-0021).
VOLUME ["/data", "/wake-models"]
EXPOSE 8084

Expand Down
70 changes: 68 additions & 2 deletions services/excita/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,59 @@

Wake-word **operations service** — label, debug, train, and configure wake-word
models. Not the runtime detector; runtime detectors POST clips into Excita.
See [`docs/specs/0011-excita-wake-word-ops.md`](../../docs/specs/0011-excita-wake-word-ops.md).
See [`docs/specs/0011-excita-wake-word-ops.md`](../../docs/specs/0011-excita-wake-word-ops.md)
and [issue #213](https://github.com/constructorfleet/conduit/issues/213) for the
microWakeWord / nanoWakeWord extension.

## Engines

Each engine honestly declares which capabilities it implements
([ADR-0020](../../docs/adr/0020-wake-engine-adapters-are-partial.md));
`GET /engines` returns the matrix, and asking for a missing capability
returns a structured `501` with `{code: "engine_capability_missing", engine,
capability, message}` ([ADR-0023](../../docs/adr/0023-engine-capability-gaps-return-501.md)):

| Engine | load/feed | score | train | package |
|----------------|-----------|-------|-------|---------------|
| openWakeWord | yes¹ | yes | — | `onnx` |
| nanoWakeWord | yes | yes | —² | `onnx` |
| microWakeWord | —³ | yes | —² | `tflite_micro`|
| Porcupine | adapter not landed yet (all gaps → 501) |

¹ Requires the shared ONNX models (`scripts/fetch-wake-models.sh`); otherwise a null slot answers 501.
² Training waits on the `EXCITA_TRAIN_WORKER_URL` worker protocol (future spec).
³ microWakeWord detects on the ESP32; Excita scores stored clips offline and packages for flash.

Phrases are engine-agnostic ([ADR-0022](../../docs/adr/0022-phrase-is-engine-agnostic.md)):
one "hey jarvis" carries models across engines, so cross-engine comparison is
one phrase row with several model rows.

## Model import

Two paths land models in Excita:

- **Upload**: `POST /models/import` (multipart artifact + JSON `metadata`
form field: `engine`, `phrase_name`, `version`, optional
`engine_phrase_key` / `metrics_json` / `notes`). Unknown phrases are
created; an `.excita.json` sidecar is written next to the stored artifact.
- **Filesystem drop**: bind-mount a directory at `EXCITA_MODEL_IMPORT_DIR`.
Scanned on boot and on `SIGHUP`; `POST /models/scan` triggers a scan too.
Each `<artifact>.excita.json` sidecar describes its artifact. The volume
is the source of truth: new files appear, removed files disappear,
sidecar `version` bumps mint new model rows (history preserved), and
re-saving without a bump changes nothing.
[ADR-0021](../../docs/adr/0021-filesystem-imported-models-are-read-only-in-ui.md):
filesystem-imported models cannot be deleted through the API (`409`,
`code: "filesystem_imported_read_only"`) — remove the file instead.

## Deploy targets

Three transports (`file`, `http_push`, `linked_service_config`) work for every
engine's native package. Publishing is one call:
`POST /deploy_targets/{id}/publish {"model_id": ...}` sets the target's current
model and pushes; a failed push never rolls back the selection (retry the same
call). `http_push` sends headers `X-Excita-Engine`, `X-Excita-Phrase`,
`X-Excita-Version`.

## Run locally

Expand Down Expand Up @@ -30,4 +82,18 @@ in `static/index.html`.
PYTHONPATH=.. pytest
```

Detection tests skip when the fetched models are missing.
Detection tests skip when the fetched models are missing. The µWW / nanoWakeWord
adapter tests skip unless their artifacts are dropped next to the openWakeWord
ones (`hey_jarvis_v0.1.tflite`, `hey_jarvis_v0.1.nww.onnx`).

## Environment

| Variable | Default | Meaning |
|---|---|---|
| `EXCITA_DATA_DIR` | `/data` | SQLite, clips, uploaded model artifacts |
| `EXCITA_BACKEND` | `sqlite` | Backend type |
| `EXCITA_BASE_URL` | `http://localhost:8084` | Advertised link URL |
| `EXCITA_WAKE_MODELS_DIR` | `<data>/wake-models` | Shared openWakeWord ONNX files |
| `EXCITA_MODEL_IMPORT_DIR` | unset | Filesystem model-import mount (scanner) |
| `EXCITA_PREROLL_MS` | `2000` | Per-source pre-roll ring buffer |
| `EXCITA_TRAIN_WORKER_URL` | referenced in 501 messages | Training worker (future spec) |
Loading
Loading