amun-bci 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- amun_bci-1.0.0/LICENSE +21 -0
- amun_bci-1.0.0/PKG-INFO +155 -0
- amun_bci-1.0.0/README.md +128 -0
- amun_bci-1.0.0/pyproject.toml +44 -0
- amun_bci-1.0.0/setup.cfg +4 -0
- amun_bci-1.0.0/src/amun/__init__.py +19 -0
- amun_bci-1.0.0/src/amun/__main__.py +160 -0
- amun_bci-1.0.0/src/amun/calibrate.py +69 -0
- amun_bci-1.0.0/src/amun/classify.py +186 -0
- amun_bci-1.0.0/src/amun/data/calibration_sample.csv +660 -0
- amun_bci-1.0.0/src/amun/engine.py +210 -0
- amun_bci-1.0.0/src/amun/features.py +63 -0
- amun_bci-1.0.0/src/amun/ingestion.py +282 -0
- amun_bci-1.0.0/src/amun/model/profile.json +9 -0
- amun_bci-1.0.0/src/amun/preprocessing.py +51 -0
- amun_bci-1.0.0/src/amun/server.py +333 -0
- amun_bci-1.0.0/src/amun/templates/index.html +472 -0
- amun_bci-1.0.0/src/amun/thinkgear.py +108 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/PKG-INFO +155 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/SOURCES.txt +28 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/dependency_links.txt +1 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/entry_points.txt +2 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/requires.txt +11 -0
- amun_bci-1.0.0/src/amun_bci.egg-info/top_level.txt +1 -0
- amun_bci-1.0.0/tests/test_classify.py +75 -0
- amun_bci-1.0.0/tests/test_engine.py +88 -0
- amun_bci-1.0.0/tests/test_features.py +50 -0
- amun_bci-1.0.0/tests/test_server.py +79 -0
- amun_bci-1.0.0/tests/test_thinkgear.py +58 -0
- amun_bci-1.0.0/tests/test_websocket.py +75 -0
amun_bci-1.0.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mohamed Mounir (Lord1Egypt)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
amun_bci-1.0.0/PKG-INFO
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: amun-bci
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Amun — a Breath–Computer Interface. Pilot a falcon with your breath. No electrodes. Just air.
|
|
5
|
+
Author-email: Mohamed Mounir <akim.221992@gmail.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Lord1Egypt/Amun
|
|
8
|
+
Project-URL: Repository, https://github.com/Lord1Egypt/Amun
|
|
9
|
+
Keywords: breath,bci,game,interface,audio,biosignal,egyptian,offline
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Games/Entertainment
|
|
14
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
|
|
15
|
+
Requires-Python: >=3.9
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Provides-Extra: mic
|
|
19
|
+
Requires-Dist: sounddevice>=0.4; extra == "mic"
|
|
20
|
+
Provides-Extra: serial
|
|
21
|
+
Requires-Dist: pyserial>=3.5; extra == "serial"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
24
|
+
Requires-Dist: numpy>=1.21; extra == "dev"
|
|
25
|
+
Requires-Dist: pillow>=9; extra == "dev"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
<div align="center">
|
|
29
|
+
|
|
30
|
+
<img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/hero.png" alt="Amun — a Breath–Computer Interface" width="100%" />
|
|
31
|
+
|
|
32
|
+
# 𓅃 Amun — a Breath–Computer Interface
|
|
33
|
+
|
|
34
|
+
**Same acronym. No electrodes. Just air.**
|
|
35
|
+
|
|
36
|
+
Pilot the falcon of Horus across the Egyptian sky using nothing but your breath.
|
|
37
|
+
Soft breath glides · a hard exhale climbs · silence dives into gravity.
|
|
38
|
+
|
|
39
|
+
<img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/demo.gif" alt="Amun gameplay" width="80%" />
|
|
40
|
+
|
|
41
|
+
</div>
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
> A ground-up reimagining of [`CoffeeIsAllYouNeed/Invisible-Driver`](https://github.com/CoffeeIsAllYouNeed/Invisible-Driver).
|
|
46
|
+
> The original was a **Brain**–Computer Interface — drive a car with EEG brain waves
|
|
47
|
+
> through an Arduino, electrodes and a clustering model. **Amun keeps the exact
|
|
48
|
+
> acronym and changes the principle:** here **BCI** means **Breath**–Computer
|
|
49
|
+
> Interface. The signal source becomes the microphone every device already has —
|
|
50
|
+
> no electrodes, no Arduino, fully offline.
|
|
51
|
+
|
|
52
|
+
## Why this is "the same idea, but better"
|
|
53
|
+
|
|
54
|
+
| | Invisible-Driver (original) | **Amun** (this repo) |
|
|
55
|
+
|---|---|---|
|
|
56
|
+
| Principle | **Brain** waves (EEG) | **Breath** (acoustic) — *and optionally brain, see below* |
|
|
57
|
+
| Hardware | Arduino + BioAmp + gel electrodes | **None** — any microphone |
|
|
58
|
+
| Acronym | Brain–Computer Interface | **Breath**–Computer Interface |
|
|
59
|
+
| Game | drive a racing car | fly the falcon of Horus over Egypt |
|
|
60
|
+
| Dependencies | Python ML stack + serial | **zero** for the core game |
|
|
61
|
+
| Runs offline | partly | **100% offline** |
|
|
62
|
+
|
|
63
|
+
The science still lives in Python — a real `ingestion → preprocessing → features →
|
|
64
|
+
classify → engine` pipeline with **k-means calibration** — but the microphone moves
|
|
65
|
+
into the browser, so the whole thing runs with **no third-party dependencies**.
|
|
66
|
+
|
|
67
|
+
## Quickstart
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
git clone https://github.com/Lord1Egypt/Amun
|
|
71
|
+
cd Amun
|
|
72
|
+
python -m amun # opens the game in your browser
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Allow the microphone and **breathe**. No microphone? Press and hold **SPACE**.
|
|
76
|
+
|
|
77
|
+
Headless / no browser (great for a quick check or CI):
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
python -m amun --source sim --duration 5 --no-input
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## How the breath becomes flight
|
|
84
|
+
|
|
85
|
+
```
|
|
86
|
+
microphone ─▶ ingestion ─▶ preprocessing ─▶ features ─▶ classify ─▶ engine ─▶ render
|
|
87
|
+
(browser) loudness noise-floor + RMS / k-means falcon canvas
|
|
88
|
+
frames EMA smoothing ZCR anchors physics
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- **Silence** → no thrust → gravity → the falcon **dives**.
|
|
92
|
+
- **Soft breath** → partial thrust → the falcon **glides** level.
|
|
93
|
+
- **Hard exhale** → full thrust → the falcon **climbs**.
|
|
94
|
+
|
|
95
|
+
Deep dive: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) ·
|
|
96
|
+
[`docs/SIGNAL_PIPELINE.md`](docs/SIGNAL_PIPELINE.md) ·
|
|
97
|
+
[`docs/CALIBRATION.md`](docs/CALIBRATION.md).
|
|
98
|
+
|
|
99
|
+
## Optional hardware (it works without any)
|
|
100
|
+
|
|
101
|
+
Amun is "the hidden one" — it accepts any *invisible* signal, but **always falls
|
|
102
|
+
back to the microphone** if no hardware is present.
|
|
103
|
+
|
|
104
|
+
| Tier | Input | What you need |
|
|
105
|
+
|---|---|---|
|
|
106
|
+
| **0 · default** | breath via browser mic | nothing |
|
|
107
|
+
| **1 · DIY** | the **Amun Amulet** — breath sensor + OLED + RGB + buzzer | Arduino/ESP32 + parts |
|
|
108
|
+
| **2 · Brain** | a **NeuroSky MindWave** (real EEG attention) | the headset |
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
amun --source serial --serial-port /dev/rfcomm0 # Amun Amulet (breath)
|
|
112
|
+
amun --source neurosky --serial-port /dev/rfcomm0 # NeuroSky (brain)
|
|
113
|
+
# if the device isn't found, Amun automatically uses the browser mic
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Tier 2 brings the original "control with your mind" idea back as a bonus — so Amun
|
|
117
|
+
is a **superset** of Invisible-Driver. Full build, BOM and wiring:
|
|
118
|
+
[`docs/HARDWARE.md`](docs/HARDWARE.md).
|
|
119
|
+
|
|
120
|
+
## Project layout
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
src/amun/ engine · ingestion · preprocessing · features · classify · calibrate · server · thinkgear
|
|
124
|
+
src/amun/templates/ the browser game (Web Audio mic + canvas renderer)
|
|
125
|
+
model/ your calibration profile (JSON)
|
|
126
|
+
tools/ sample data · demo-gif · banner/logo · Gemini asset gen · test runner
|
|
127
|
+
tests/ pytest suite (engine · features · classify · websocket · server · thinkgear)
|
|
128
|
+
notebooks/ breath-signal exploration + honest clustering metric
|
|
129
|
+
docs/ architecture & guides hardware/ Amun Amulet sketch + BOM
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
## Tests
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
pip install -e ".[dev]"
|
|
136
|
+
python tools/test_all.py # data + calibration + headless run + pytest (exit 0)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
See [`TESTING.md`](TESTING.md). Everything runs with **no microphone and no hardware**.
|
|
140
|
+
|
|
141
|
+
## Honesty note
|
|
142
|
+
|
|
143
|
+
All performance/quality numbers (e.g. the calibration **silhouette score**) are
|
|
144
|
+
**measured** on bundled, reproducible data — never invented. Your own calibration
|
|
145
|
+
produces a score for your microphone and breathing.
|
|
146
|
+
|
|
147
|
+
## Status
|
|
148
|
+
|
|
149
|
+
Live build log: [`CHECKPOINTS.md`](CHECKPOINTS.md).
|
|
150
|
+
|
|
151
|
+
## License
|
|
152
|
+
|
|
153
|
+
MIT © 2026 Mohamed Mounir ([Lord1Egypt](https://github.com/Lord1Egypt))
|
|
154
|
+
|
|
155
|
+
<div align="center"><img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/logo.png" alt="Amun" width="120" /></div>
|
amun_bci-1.0.0/README.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/hero.png" alt="Amun — a Breath–Computer Interface" width="100%" />
|
|
4
|
+
|
|
5
|
+
# 𓅃 Amun — a Breath–Computer Interface
|
|
6
|
+
|
|
7
|
+
**Same acronym. No electrodes. Just air.**
|
|
8
|
+
|
|
9
|
+
Pilot the falcon of Horus across the Egyptian sky using nothing but your breath.
|
|
10
|
+
Soft breath glides · a hard exhale climbs · silence dives into gravity.
|
|
11
|
+
|
|
12
|
+
<img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/demo.gif" alt="Amun gameplay" width="80%" />
|
|
13
|
+
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
> A ground-up reimagining of [`CoffeeIsAllYouNeed/Invisible-Driver`](https://github.com/CoffeeIsAllYouNeed/Invisible-Driver).
|
|
19
|
+
> The original was a **Brain**–Computer Interface — drive a car with EEG brain waves
|
|
20
|
+
> through an Arduino, electrodes and a clustering model. **Amun keeps the exact
|
|
21
|
+
> acronym and changes the principle:** here **BCI** means **Breath**–Computer
|
|
22
|
+
> Interface. The signal source becomes the microphone every device already has —
|
|
23
|
+
> no electrodes, no Arduino, fully offline.
|
|
24
|
+
|
|
25
|
+
## Why this is "the same idea, but better"
|
|
26
|
+
|
|
27
|
+
| | Invisible-Driver (original) | **Amun** (this repo) |
|
|
28
|
+
|---|---|---|
|
|
29
|
+
| Principle | **Brain** waves (EEG) | **Breath** (acoustic) — *and optionally brain, see below* |
|
|
30
|
+
| Hardware | Arduino + BioAmp + gel electrodes | **None** — any microphone |
|
|
31
|
+
| Acronym | Brain–Computer Interface | **Breath**–Computer Interface |
|
|
32
|
+
| Game | drive a racing car | fly the falcon of Horus over Egypt |
|
|
33
|
+
| Dependencies | Python ML stack + serial | **zero** for the core game |
|
|
34
|
+
| Runs offline | partly | **100% offline** |
|
|
35
|
+
|
|
36
|
+
The science still lives in Python — a real `ingestion → preprocessing → features →
|
|
37
|
+
classify → engine` pipeline with **k-means calibration** — but the microphone moves
|
|
38
|
+
into the browser, so the whole thing runs with **no third-party dependencies**.
|
|
39
|
+
|
|
40
|
+
## Quickstart
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/Lord1Egypt/Amun
|
|
44
|
+
cd Amun
|
|
45
|
+
python -m amun # opens the game in your browser
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Allow the microphone and **breathe**. No microphone? Press and hold **SPACE**.
|
|
49
|
+
|
|
50
|
+
Headless / no browser (great for a quick check or CI):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
python -m amun --source sim --duration 5 --no-input
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## How the breath becomes flight
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
microphone ─▶ ingestion ─▶ preprocessing ─▶ features ─▶ classify ─▶ engine ─▶ render
|
|
60
|
+
(browser) loudness noise-floor + RMS / k-means falcon canvas
|
|
61
|
+
frames EMA smoothing ZCR anchors physics
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
- **Silence** → no thrust → gravity → the falcon **dives**.
|
|
65
|
+
- **Soft breath** → partial thrust → the falcon **glides** level.
|
|
66
|
+
- **Hard exhale** → full thrust → the falcon **climbs**.
|
|
67
|
+
|
|
68
|
+
Deep dive: [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) ·
|
|
69
|
+
[`docs/SIGNAL_PIPELINE.md`](docs/SIGNAL_PIPELINE.md) ·
|
|
70
|
+
[`docs/CALIBRATION.md`](docs/CALIBRATION.md).
|
|
71
|
+
|
|
72
|
+
## Optional hardware (it works without any)
|
|
73
|
+
|
|
74
|
+
Amun is "the hidden one" — it accepts any *invisible* signal, but **always falls
|
|
75
|
+
back to the microphone** if no hardware is present.
|
|
76
|
+
|
|
77
|
+
| Tier | Input | What you need |
|
|
78
|
+
|---|---|---|
|
|
79
|
+
| **0 · default** | breath via browser mic | nothing |
|
|
80
|
+
| **1 · DIY** | the **Amun Amulet** — breath sensor + OLED + RGB + buzzer | Arduino/ESP32 + parts |
|
|
81
|
+
| **2 · Brain** | a **NeuroSky MindWave** (real EEG attention) | the headset |
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
amun --source serial --serial-port /dev/rfcomm0 # Amun Amulet (breath)
|
|
85
|
+
amun --source neurosky --serial-port /dev/rfcomm0 # NeuroSky (brain)
|
|
86
|
+
# if the device isn't found, Amun automatically uses the browser mic
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Tier 2 brings the original "control with your mind" idea back as a bonus — so Amun
|
|
90
|
+
is a **superset** of Invisible-Driver. Full build, BOM and wiring:
|
|
91
|
+
[`docs/HARDWARE.md`](docs/HARDWARE.md).
|
|
92
|
+
|
|
93
|
+
## Project layout
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
src/amun/ engine · ingestion · preprocessing · features · classify · calibrate · server · thinkgear
|
|
97
|
+
src/amun/templates/ the browser game (Web Audio mic + canvas renderer)
|
|
98
|
+
model/ your calibration profile (JSON)
|
|
99
|
+
tools/ sample data · demo-gif · banner/logo · Gemini asset gen · test runner
|
|
100
|
+
tests/ pytest suite (engine · features · classify · websocket · server · thinkgear)
|
|
101
|
+
notebooks/ breath-signal exploration + honest clustering metric
|
|
102
|
+
docs/ architecture & guides hardware/ Amun Amulet sketch + BOM
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Tests
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
pip install -e ".[dev]"
|
|
109
|
+
python tools/test_all.py # data + calibration + headless run + pytest (exit 0)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
See [`TESTING.md`](TESTING.md). Everything runs with **no microphone and no hardware**.
|
|
113
|
+
|
|
114
|
+
## Honesty note
|
|
115
|
+
|
|
116
|
+
All performance/quality numbers (e.g. the calibration **silhouette score**) are
|
|
117
|
+
**measured** on bundled, reproducible data — never invented. Your own calibration
|
|
118
|
+
produces a score for your microphone and breathing.
|
|
119
|
+
|
|
120
|
+
## Status
|
|
121
|
+
|
|
122
|
+
Live build log: [`CHECKPOINTS.md`](CHECKPOINTS.md).
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT © 2026 Mohamed Mounir ([Lord1Egypt](https://github.com/Lord1Egypt))
|
|
127
|
+
|
|
128
|
+
<div align="center"><img src="https://raw.githubusercontent.com/Lord1Egypt/Amun/main/assets/logo.png" alt="Amun" width="120" /></div>
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "amun-bci"
|
|
7
|
+
version = "1.0.0"
|
|
8
|
+
description = "Amun — a Breath–Computer Interface. Pilot a falcon with your breath. No electrodes. Just air."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Mohamed Mounir", email = "akim.221992@gmail.com" }]
|
|
13
|
+
keywords = ["breath", "bci", "game", "interface", "audio", "biosignal", "egyptian", "offline"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Programming Language :: Python :: 3",
|
|
16
|
+
"License :: OSI Approved :: MIT License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Topic :: Games/Entertainment",
|
|
19
|
+
"Topic :: Multimedia :: Sound/Audio :: Analysis",
|
|
20
|
+
]
|
|
21
|
+
# The core game has ZERO third-party dependencies (the browser captures the mic).
|
|
22
|
+
dependencies = []
|
|
23
|
+
|
|
24
|
+
[project.optional-dependencies]
|
|
25
|
+
mic = ["sounddevice>=0.4"] # optional headless/CLI microphone capture
|
|
26
|
+
serial = ["pyserial>=3.5"] # optional Amun Amulet / NeuroSky hardware link
|
|
27
|
+
dev = ["pytest>=7", "numpy>=1.21", "pillow>=9"] # tests, calibration ML, demo gif
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/Lord1Egypt/Amun"
|
|
31
|
+
Repository = "https://github.com/Lord1Egypt/Amun"
|
|
32
|
+
|
|
33
|
+
[project.scripts]
|
|
34
|
+
amun = "amun.__main__:main"
|
|
35
|
+
|
|
36
|
+
[tool.setuptools.packages.find]
|
|
37
|
+
where = ["src"]
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.package-data]
|
|
40
|
+
amun = ["templates/*.html", "model/*.json", "data/*.csv"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
44
|
+
addopts = "-q"
|
amun_bci-1.0.0/setup.cfg
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""Amun — a Breath–Computer Interface (BCI).
|
|
2
|
+
|
|
3
|
+
The original *Invisible-Driver* was a **Brain**–Computer Interface: drive a car
|
|
4
|
+
with EEG brain waves. Amun keeps the exact acronym and changes the principle —
|
|
5
|
+
**BCI** here means **Breath**–Computer Interface. You pilot a falcon through an
|
|
6
|
+
Egyptian sky using nothing but the microphone every device already has.
|
|
7
|
+
|
|
8
|
+
Soft breath glides, a hard exhale climbs, silence dives into gravity.
|
|
9
|
+
|
|
10
|
+
The package mirrors the original's pipeline:
|
|
11
|
+
|
|
12
|
+
ingestion -> preprocessing -> features -> classify -> engine -> render
|
|
13
|
+
|
|
14
|
+
Named for **Amun**, the Egyptian god of air and the hidden/invisible — a mirror
|
|
15
|
+
of "Invisible Driver."
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
__version__ = "1.0.0"
|
|
19
|
+
__all__ = ["__version__"]
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""Command-line entry point for Amun.
|
|
2
|
+
|
|
3
|
+
amun # serve the browser game (mic in the browser)
|
|
4
|
+
amun --no-browser # serve without opening a browser
|
|
5
|
+
amun --source sim --duration 5 # headless run, no browser, no mic
|
|
6
|
+
amun --source mic # headless run using the optional sounddevice mic
|
|
7
|
+
amun --source replay --file f.csv # headless run from recorded loudness
|
|
8
|
+
amun calibrate --from data.csv # fit a calibration profile (non-blocking)
|
|
9
|
+
|
|
10
|
+
Every mode is non-blocking when given arguments: nothing waits on ``input()``,
|
|
11
|
+
so CI and the test runner never hang.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
|
|
21
|
+
from .calibrate import DEFAULT_PROFILE_PATH, calibrate_cli, load_or_default
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _banner() -> str:
|
|
25
|
+
return (
|
|
26
|
+
"\n 𓅃 A M U N — Breath–Computer Interface\n"
|
|
27
|
+
" pilot a falcon with your breath · no electrodes · just air\n"
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
32
|
+
p = argparse.ArgumentParser(prog="amun", description="Amun — a Breath–Computer Interface.")
|
|
33
|
+
sub = p.add_subparsers(dest="command")
|
|
34
|
+
|
|
35
|
+
# default: serve / run
|
|
36
|
+
p.add_argument("--source",
|
|
37
|
+
choices=["browser", "sim", "replay", "mic", "serial", "neurosky"],
|
|
38
|
+
default="browser",
|
|
39
|
+
help="signal source (default: browser mic). 'serial'/'neurosky' "
|
|
40
|
+
"are optional hardware; they fall back to the mic if absent")
|
|
41
|
+
p.add_argument("--host", default="127.0.0.1")
|
|
42
|
+
p.add_argument("--port", type=int, default=8011)
|
|
43
|
+
p.add_argument("--file", type=Path, help="loudness file for --source replay")
|
|
44
|
+
p.add_argument("--serial-port", default=None,
|
|
45
|
+
help="serial/Bluetooth port for --source serial|neurosky "
|
|
46
|
+
"(e.g. /dev/ttyUSB0, /dev/rfcomm0, COM5)")
|
|
47
|
+
p.add_argument("--baud", type=int, default=None, help="serial baud rate override")
|
|
48
|
+
p.add_argument("--duration", type=float, default=None,
|
|
49
|
+
help="seconds to run a headless source then exit")
|
|
50
|
+
p.add_argument("--no-browser", action="store_true", help="don't auto-open a browser")
|
|
51
|
+
p.add_argument("--no-input", action="store_true",
|
|
52
|
+
help="never wait for keyboard input (for automation)")
|
|
53
|
+
p.add_argument("--quiet", action="store_true")
|
|
54
|
+
|
|
55
|
+
cal = sub.add_parser("calibrate", help="fit a calibration profile (non-blocking)")
|
|
56
|
+
cal.add_argument("--from", dest="from_file", type=Path, default=None,
|
|
57
|
+
help="loudness file (one float/line); defaults to bundled sample")
|
|
58
|
+
cal.add_argument("--out", type=Path, default=DEFAULT_PROFILE_PATH)
|
|
59
|
+
return p
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None) -> int:
|
|
63
|
+
args = build_parser().parse_args(argv)
|
|
64
|
+
|
|
65
|
+
if args.command == "calibrate":
|
|
66
|
+
profile = calibrate_cli(source_file=args.from_file, save_to=args.out)
|
|
67
|
+
print(f"Calibrated from {profile.n_frames} frames "
|
|
68
|
+
f"(silhouette={profile.silhouette:.3f})")
|
|
69
|
+
print(f" noise_floor={profile.noise_floor} soft={profile.soft} "
|
|
70
|
+
f"hard={profile.hard}")
|
|
71
|
+
print(f"Saved profile -> {args.out}")
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
profile = load_or_default()
|
|
75
|
+
|
|
76
|
+
# Headless sources run the engine directly without a browser.
|
|
77
|
+
if args.source in ("sim", "replay", "mic", "serial", "neurosky"):
|
|
78
|
+
from .ingestion import make_source
|
|
79
|
+
from .server import run_headless
|
|
80
|
+
|
|
81
|
+
kwargs = {}
|
|
82
|
+
if args.source == "replay":
|
|
83
|
+
if not args.file:
|
|
84
|
+
print("error: --source replay requires --file", file=sys.stderr)
|
|
85
|
+
return 2
|
|
86
|
+
kwargs["path"] = args.file
|
|
87
|
+
if args.source in ("serial", "neurosky"):
|
|
88
|
+
if not args.serial_port:
|
|
89
|
+
print(f"error: --source {args.source} requires --serial-port",
|
|
90
|
+
file=sys.stderr)
|
|
91
|
+
return 2
|
|
92
|
+
kwargs["port"] = args.serial_port
|
|
93
|
+
if args.baud:
|
|
94
|
+
kwargs["baud"] = args.baud
|
|
95
|
+
|
|
96
|
+
source = make_source(args.source, **kwargs)
|
|
97
|
+
|
|
98
|
+
# Hardware sources may be absent — start them first and, if that fails,
|
|
99
|
+
# fall back to the always-available browser microphone. "Works anyway."
|
|
100
|
+
started = False
|
|
101
|
+
if args.source in ("serial", "neurosky", "mic"):
|
|
102
|
+
try:
|
|
103
|
+
source.start()
|
|
104
|
+
started = True
|
|
105
|
+
except RuntimeError as exc:
|
|
106
|
+
print(f"\n ⚠ {args.source} unavailable:\n "
|
|
107
|
+
f"{str(exc).splitlines()[0]}")
|
|
108
|
+
print(" → falling back to the browser microphone.\n")
|
|
109
|
+
return _serve_browser(args, profile)
|
|
110
|
+
|
|
111
|
+
if not args.quiet:
|
|
112
|
+
print(_banner())
|
|
113
|
+
print(f" running headless source={args.source} "
|
|
114
|
+
f"duration={args.duration or '∞'}\n")
|
|
115
|
+
final = run_headless(source, profile=profile, duration=args.duration,
|
|
116
|
+
quiet=args.quiet, already_started=started)
|
|
117
|
+
if not args.quiet:
|
|
118
|
+
print(f" final score: {final['score']} ankhs: {final['ankhs']}")
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
# Default: browser game.
|
|
122
|
+
return _serve_browser(args, profile)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _serve_browser(args, profile) -> int:
|
|
126
|
+
"""Serve the browser game (microphone in the page). The guaranteed path."""
|
|
127
|
+
from .server import run_server
|
|
128
|
+
|
|
129
|
+
httpd = run_server(host=args.host, port=args.port, profile=profile,
|
|
130
|
+
open_browser=not args.no_browser)
|
|
131
|
+
url = f"http://{args.host}:{httpd.server_address[1]}/"
|
|
132
|
+
if not args.quiet:
|
|
133
|
+
print(_banner())
|
|
134
|
+
print(f" 🜂 serving at {url}")
|
|
135
|
+
print(" open it, allow the microphone, and breathe.")
|
|
136
|
+
print(" (no mic? press & hold SPACE to breathe. Ctrl+C to stop.)\n")
|
|
137
|
+
|
|
138
|
+
if args.no_input:
|
|
139
|
+
# bounded run for automation
|
|
140
|
+
import threading
|
|
141
|
+
|
|
142
|
+
deadline = time.monotonic() + (args.duration or 1.0)
|
|
143
|
+
t = threading.Thread(target=httpd.serve_forever, daemon=True)
|
|
144
|
+
t.start()
|
|
145
|
+
while time.monotonic() < deadline:
|
|
146
|
+
time.sleep(0.1)
|
|
147
|
+
httpd.shutdown()
|
|
148
|
+
return 0
|
|
149
|
+
|
|
150
|
+
try:
|
|
151
|
+
httpd.serve_forever()
|
|
152
|
+
except KeyboardInterrupt:
|
|
153
|
+
print("\n 𓂀 may Ma'at weigh your flight kindly. Goodbye.")
|
|
154
|
+
finally:
|
|
155
|
+
httpd.shutdown()
|
|
156
|
+
return 0
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
if __name__ == "__main__":
|
|
160
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Calibration — learn your personal silence / soft / hard breath levels.
|
|
2
|
+
|
|
3
|
+
Two entry points:
|
|
4
|
+
|
|
5
|
+
* :func:`calibrate_from_samples` — fit a profile from loudness frames you already
|
|
6
|
+
have (used by the browser calibration step, the notebook and the tests).
|
|
7
|
+
* :func:`calibrate_cli` — a non-blocking helper that fits from a replay/sample
|
|
8
|
+
file or from bundled sample data, so automation never hangs on ``input()``
|
|
9
|
+
(per the project's CLI rules).
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional, Sequence
|
|
16
|
+
|
|
17
|
+
from .classify import CalibrationProfile, fit_profile
|
|
18
|
+
|
|
19
|
+
PKG = Path(__file__).resolve().parent
|
|
20
|
+
# Where a calibrated profile is written (user-writable, works when pip-installed).
|
|
21
|
+
DEFAULT_PROFILE_PATH = Path.home() / ".config" / "amun" / "profile.json"
|
|
22
|
+
# Read-only resources bundled inside the package (shipped in the wheel).
|
|
23
|
+
BUNDLED_PROFILE = PKG / "model" / "profile.json"
|
|
24
|
+
BUNDLED_SAMPLE = PKG / "data" / "calibration_sample.csv"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def calibrate_from_samples(samples: Sequence[float],
|
|
28
|
+
save_to: Optional[Path] = None) -> CalibrationProfile:
|
|
29
|
+
"""Fit and (optionally) save a calibration profile from loudness frames."""
|
|
30
|
+
profile = fit_profile(samples)
|
|
31
|
+
if save_to is not None:
|
|
32
|
+
Path(save_to).parent.mkdir(parents=True, exist_ok=True)
|
|
33
|
+
profile.save(save_to)
|
|
34
|
+
return profile
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def load_or_default(path: Optional[Path] = None) -> CalibrationProfile:
|
|
38
|
+
"""Load the saved profile; fall back to the bundled one, then to defaults."""
|
|
39
|
+
for candidate in (path or DEFAULT_PROFILE_PATH, BUNDLED_PROFILE):
|
|
40
|
+
candidate = Path(candidate)
|
|
41
|
+
if candidate.exists():
|
|
42
|
+
try:
|
|
43
|
+
return CalibrationProfile.load(candidate)
|
|
44
|
+
except Exception:
|
|
45
|
+
continue
|
|
46
|
+
return CalibrationProfile.default()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _read_values(path: Path) -> list:
|
|
50
|
+
return [
|
|
51
|
+
float(line)
|
|
52
|
+
for line in Path(path).read_text().splitlines()
|
|
53
|
+
if line.strip()
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def calibrate_cli(source_file: Optional[Path] = None,
|
|
58
|
+
save_to: Optional[Path] = None) -> CalibrationProfile:
|
|
59
|
+
"""Non-blocking calibration for scripts/CI.
|
|
60
|
+
|
|
61
|
+
Reads loudness values from ``source_file`` (one float per line). If none is
|
|
62
|
+
given, uses the bundled sample dataset so it always succeeds offline.
|
|
63
|
+
"""
|
|
64
|
+
if source_file is not None:
|
|
65
|
+
values = _read_values(Path(source_file))
|
|
66
|
+
else:
|
|
67
|
+
values = _read_values(BUNDLED_SAMPLE) if BUNDLED_SAMPLE.exists() else []
|
|
68
|
+
profile = calibrate_from_samples(values, save_to=save_to or DEFAULT_PROFILE_PATH)
|
|
69
|
+
return profile
|