Skip to content
Draft
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
6 changes: 3 additions & 3 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

- [ ] Commits are signed off (`git commit -s`). See CONTRIBUTING.md.
- [ ] `python tools/check_layering.py .` and `python tools/release_check.py .` pass.
- [ ] `pytest` passes in `emet-sdk`, `emet-hal` and `emet-engine`.
- [ ] `pytest` passes in `emet-sdk`, `emet-hal`, `emet-providers` and `emet-engine`.
- [ ] New behaviour has a test and a caller.
- [ ] Anything taken from a paper or repository is cited at the point of use
and in CITATIONS.md.
Expand All @@ -34,7 +34,7 @@ even when it works. See DESIGN.md section 2.

- [ ] The soul names no hardware.
- [ ] Every fallback chain still terminates in a voice rung.
- [ ] `emet_sdk` imports nothing internal; `emet_hal` and `emet_engine`
import `emet_sdk` only.
- [ ] `emet_sdk` imports nothing internal; `emet_hal`, `emet_providers` and
`emet_engine` import `emet_sdk` only.
- [ ] Memory is not namespaced by body.
- [ ] A missing plugin is still distinct from a schema error.
10 changes: 10 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,13 @@ updates:
patterns: ["*"]
commit-message:
prefix: "build"

- package-ecosystem: pip
directory: /emet-providers
schedule:
interval: monthly
groups:
python:
patterns: ["*"]
commit-message:
prefix: "build"
88 changes: 75 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
# Two things are checked, and the second matters more than it looks.
#
# tests: the 0.1 acceptance criteria, executable.
# layering: emet_sdk imports nothing internal; emet_hal and emet_engine
# import emet_sdk only. The same job runs the release
# invariants and the house-style check.
# layering: emet_sdk imports nothing internal; emet_hal, emet_providers
# and emet_engine import emet_sdk only. The same job runs the
# release invariants and the house-style check.
#
# The layering check exists because the closed-engine plan used to enforce that
# boundary structurally, and an open monorepo does not. See DISTRIBUTION.md §2.
Expand Down Expand Up @@ -41,7 +41,7 @@ jobs:
- name: Check package layering
run: python tools/check_layering.py .

# Cross-package invariants no single suite can see: the three packages
# Cross-package invariants no single suite can see: the four packages
# agreeing about the version, every discovery group having something
# behind it, and no document advertising a version the code is not.
- name: Check release invariants
Expand Down Expand Up @@ -105,6 +105,7 @@ jobs:
python -m pip install --upgrade build
python -m build --wheel emet-sdk
python -m build --wheel emet-hal
python -m build --wheel emet-providers
python -m build --wheel emet-engine

- name: Data files are actually inside the wheel
Expand All @@ -131,7 +132,7 @@ jobs:

- name: Install the wheels, not the source
run: |
python -m pip install emet-sdk/dist/*.whl emet-hal/dist/*.whl emet-engine/dist/*.whl
python -m pip install emet-sdk/dist/*.whl emet-hal/dist/*.whl emet-providers/dist/*.whl emet-engine/dist/*.whl

# `cd /tmp` is the point of this step: from here the source checkout is
# not on the path, so anything that resolves must be coming out of the
Expand Down Expand Up @@ -159,14 +160,22 @@ jobs:
assert r.has_wake("pocketsphinx"), "wake group did not survive packaging"
assert r.has_audio("microphone"), "audio group did not survive packaging"
assert r.has_audio_out("speaker"), "audio_out group did not survive packaging"
assert r.has_stt("mock"), "stt group did not survive packaging"
assert r.has_llm("mock"), "llm group did not survive packaging"
assert r.has_tts("mock"), "tts group did not survive packaging"
print("discovered:", [f"{g}:{n}" for g, n in r])

# The version is declared once, in pyproject.toml, and read back
# through importlib.metadata. From a wheel that resolution path is
# different from an editable install, so it is worth checking here
# rather than only in the test suite.
import emet_sdk, emet_hal, emet_engine
for dist, mod in (("emet-sdk", emet_sdk), ("emet-hal", emet_hal), ("emet-engine", emet_engine)):
import emet_sdk, emet_hal, emet_providers, emet_engine
for dist, mod in (
("emet-sdk", emet_sdk),
("emet-hal", emet_hal),
("emet-providers", emet_providers),
("emet-engine", emet_engine),
):
assert mod.__version__ == version(dist), f"{dist} version drifted"
assert mod.__version__ != "0+unknown", f"{dist} reports no version"
print("versions:", emet_sdk.__version__)
Expand All @@ -186,13 +195,15 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip

# emet-hal is installed too, and not only so its own tests run: plugin
# discovery reads entry points, so several SDK tests are meaningless
# unless something is actually registered to discover.
# emet-hal and emet-providers are installed too, and not only so their
# own tests run: plugin discovery reads entry points, so several SDK and
# engine tests are meaningless unless something is actually registered
# to discover.
- name: Install packages
run: |
python -m pip install -e "emet-sdk[dev]"
python -m pip install -e "emet-hal[dev]"
python -m pip install -e "emet-providers[dev,deepgram,anthropic,openai]"
python -m pip install -e "emet-engine[dev]"

# The shipped wake engine, which is an optional extra rather than a hard
Expand All @@ -213,9 +224,17 @@ jobs:
working-directory: emet-hal
run: python -m pytest -q

# Runs without a microphone or an acoustic model: the loop is exercised
# through a wav file and the mock detector, both reached by the same
# entry-point discovery the real ones use.
# No network and no key. The mocks need nothing, and every real
# provider is tested against a stand-in server; the live tests skip
# unless a key and EMET_LIVE_TESTS=1 are set.
- name: Test emet-providers
working-directory: emet-providers
run: python -m pytest -q

# Runs without a microphone, an acoustic model or a network: the loop is
# exercised through a wav file, the mock detector and the mock
# transcriber, all reached by the same entry-point discovery the real
# ones use.
- name: Test emet-engine
working-directory: emet-engine
run: python -m pytest -q
Expand Down Expand Up @@ -248,3 +267,46 @@ jobs:
fi
done
echo "all invalid fixtures rejected"

# The same four suites with every optional extra absent: no pocketsphinx,
# no sounddevice, no websockets, no httpx, no piper-tts. A body that only
# ever wakes installs none of them, and every test that needs one has to
# skip rather than fail. This used to be a rule that lived on one laptop
# (shadow the modules by hand before every push); now it is a job.
bare:
name: test (no extras)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
with:
python-version: "3.13"
cache: pip
- name: Install the packages and nothing optional
run: |
python -m pip install -e "emet-sdk[dev]"
python -m pip install -e "emet-hal[dev]"
python -m pip install -e "emet-providers[dev]"
python -m pip install -e "emet-engine[dev]"
- name: Every optional extra is really absent
run: |
python - <<'PY'
import importlib.util, sys
present = [m for m in ("pocketsphinx", "sounddevice", "websockets", "httpx", "piper") if importlib.util.find_spec(m)]
if present:
print("optional extras leaked into the bare job:", present)
sys.exit(1)
print("bare: no optional extras installed")
PY
- name: Test emet-sdk
working-directory: emet-sdk
run: python -m pytest -q
- name: Test emet-hal
working-directory: emet-hal
run: python -m pytest -q
- name: Test emet-providers
working-directory: emet-providers
run: python -m pytest -q
- name: Test emet-engine
working-directory: emet-engine
run: python -m pytest -q
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@ __pycache__/
dist/
build/
.pytest_cache/

# Keys are bring-your-own and never enter history, whatever they are called.
keys.env
.env
31 changes: 31 additions & 0 deletions CITATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,37 @@ firing.

---

## Piper, and the LJ Speech voice

**Michael Hansen and the Open Home Foundation.** Piper, a fast local neural
text-to-speech engine. <https://github.com/OHF-Voice/piper1-gpl>,
**GPL-3.0-or-later** (the package `piper-tts`, 1.8.0 on 2026-09-04). The
licence is the GPL because espeak-ng, the phonemiser, is compiled into the
wheel; the earlier `rhasspy/piper` was MIT and is no longer where releases
come from. Depends on ONNX Runtime (Microsoft, **MIT**).

The shipped local voice
([`emet_providers/piper.py`](emet-providers/emet_providers/piper.py)), used as
a dependency behind the optional extra `emet-providers[piper]`. Emet imports
it and does not copy, bundle or redistribute it, so the Apache-2.0 terms of
this repository are unaffected and a body installed without the extra
carries no GPL code. Anyone packaging Emet *with* Piper inside one
distribution takes on the GPL's terms for that distribution, and should know
it. Facts verified against PyPI and the repository on 2026-09-15.

**Keith Ito and Linda Johnson.** The LJ Speech Dataset.
<https://keithito.com/LJ-Speech-Dataset/>, **public domain**. The reference
soul's voice, `en_US-ljspeech-medium`, is a Piper model trained on it
(model card in `rhasspy/piper-voices` on Hugging Face, read 2026-09-15). It
was chosen over Amy, the earlier reference voice, because Amy's model card
says it is fine-tuned from the Lessac voice, and the Blizzard 2013 Lessac
corpus licence excludes "the development, marketing, commercialisation, sale
or licencing of voice synthesis" products; a default a project promises
people may fork and sell cannot rest on that. The voice files themselves are
downloaded by the owner and are not in this repository.

---

## Adding to this file

If a change takes an idea, a finding, a number, or a data format from outside
Expand Down
14 changes: 9 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ are cheaper to get wrong.

- **Drivers** in `emet-hal`: servos, displays, LEDs, sensors, motor drivers.
None are written yet; `emet_hal.mock` shows the shape one takes.
- **Providers** in `emet-providers`: speech recognition today, language
models and voices later. `emet_providers.mock` shows the shape, and
`DESIGN.md` section 12.3 says who chooses one.
- **Locomotion plugins**: new kinematics. `drive.kinematics` is an open enum
precisely so that `legged`, `omni`, and things nobody has thought of can
arrive as packages rather than as schema changes.
Expand Down Expand Up @@ -117,8 +120,8 @@ wrong even if it works:
2. **Every fallback chain terminates in a voice rung.** The validator enforces
this. It is what makes "every intent is always satisfiable" mechanical
rather than aspirational.
3. **`emet_sdk` imports nothing internal.** `emet_hal` and `emet_engine` import
`emet_sdk` only. CI checks this on every pull request.
3. **`emet_sdk` imports nothing internal.** `emet_hal`, `emet_providers` and
`emet_engine` import `emet_sdk` only. CI checks this on every pull request.
4. **Memory is never namespaced by body.** Experiences travel with the soul;
hardware conditions stay with the body.
5. **A missing plugin is not a schema error.** Keep the two failure modes
Expand Down Expand Up @@ -171,11 +174,11 @@ Both run in CI, so they cannot quietly stop working.

```sh
python -m venv .venv
.venv/bin/pip install -e "emet-sdk[dev]" -e "emet-hal[dev]" -e "emet-engine[dev]"
.venv/bin/pip install -e "emet-sdk[dev]" -e "emet-hal[dev]" -e "emet-providers[dev]" -e "emet-engine[dev]"
.venv/bin/pip install -e "emet-hal[audio,wake]" # optional: microphone, speaker, wake engine
```

Install **all three** packages even if you are only touching one. Plugin
Install **all four** packages even if you are only touching one. Plugin
discovery reads entry points, so several SDK and engine tests are meaningless
unless something is registered to be discovered. The suites pass with the
optional extras absent; the tests that need them skip.
Expand All @@ -188,6 +191,7 @@ python tools/release_check.py .
python tools/check_style.py .
cd emet-sdk && python -m pytest -q
cd ../emet-hal && python -m pytest -q
cd ../emet-providers && python -m pytest -q
cd ../emet-engine && python -m pytest -q
```

Expand All @@ -198,7 +202,7 @@ a comma, a colon, or two sentences. None of the six words that read as a
press release; `tools/check_style.py` lists them, enforces both rules, and
runs in CI, so a stray dash fails the build rather than a review.

CI also builds the three wheels and installs them outside the source tree, so a
CI also builds the four wheels and installs them outside the source tree, so a
packaging mistake that an editable install hides still fails the pipeline. The
exact steps are in `.github/workflows/ci.yml`.

Expand Down
Loading