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 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.
@@ -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>
@@ -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"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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