Privacy-first, fully on-device macOS prototype for EEG-driven communication. A Muse headband streams brain signals through BrainFlow, a Core ML classifier on the Apple Neural Engine detects intent (jaw clench / blink / rest / select), and a local MLX LLM suggests next words. The user "types" by letting a cycling 3-token carousel highlight a candidate, then committing with a brain-signal selection. No network at runtime. No cloud. No telemetry.
- Swift 6.0, macOS 14+, SwiftPM (no Xcode project needed for the default build)
- Deps:
mlx-swift(≥0.21.2),mlx-swift-examples(≥2.21.0),swift-transformers(≥0.1.20). Linked only intoBCILLM— single MLX runtime in the linked binary. - BrainFlow is NOT a SwiftPM dep — it's an optional system library via the
BCIBridgeObj-C++ shim, gated byBCI_BRAINFLOW_AVAILABLE - Strict concurrency on all targets;
ExistentialAnyupcoming feature enabled
./Scripts/build.sh # swift build (stub bridge, mock classifier, stub predictor)
./Scripts/run-synthetic.sh # launches app with synthetic EEG — no hardware, no models needed
./Scripts/build.sh --with-brainflow # requires BrainFlow installed (see HARDWARE_SETUP.md)
./Scripts/run-muse-s.sh # live Muse S over Bluetooth
./Scripts/build-xcode-mlx.sh # MLX path needs full Xcode (not just CLT) to compile Metal kernels
./Scripts/run-calibration.sh # capture training data for intent classifier
python Scripts/train-intent-classifier.py # train + export an .mlpackage from calibration data
swift test # all testsSources/BCIBridge/— Obj-C++ shim for BrainFlow. Default builds withBCI_BRIDGE_STUB; defineBCI_BRAINFLOW_AVAILABLE+-lBrainflowto wire in the real library.Sources/BCICore/— pure-Swift, no third-party deps: models, protocols (EEGStreaming,IntentClassifying,NextWordPredicting,TokenizerProviding), ring buffer, intent FSMSources/BCIEEG/— EEG streams: BrainFlow facade, synthetic generator, playback from recordingsSources/BCIClassifier/— Core ML wrapper (ANE-preferred viaMLComputeUnits); mock classifier for stub modeSources/BCILLM/— isolated MLX adapter + stub predictor + tokenizer; ONLY target that imports MLX/transformersSources/NeuralComposeApp/— SwiftUI app target;AppContainercomposes everything via protocolsModels/—IntentClassifier.mlpackage(auto-compiled by Core ML on first load), plusREADME.mdfor the MLX LLM drop-in conventionTests/— one test target per library (BCICoreTests,BCIEEGTests,BCIClassifierTests,BCILLMTests)Scripts/— build/run/calibration/training shell scripts + Python training helperRecordings/— captured EEG sessions for playback testingWorldModel/— decoupled research spike (PyTorch, not MLX; no dependency onSources/): a synthetic JEPA + latent-MPC exercise, unrelated to the EEG pipeline until proven out — see its ownREADME.mdHARDWARE_SETUP.md,MODEL_SETUP.md,CALIBRATION.md,TROUBLESHOOTING.md— operational docs (keep up to date)
- Composition root is
AppContainer— that's where dependency overrides for tests and the menu-bar UI live - App talks to subsystems through protocols only. Never
import MLXoutsideBCILLM. - MLX isolation is load-bearing. Linking MLX into multiple targets duplicates the runtime — there's a Package.swift comment about it.
- Strict concurrency everywhere —
@Sendable/actor discipline expected - Stub-by-default: missing model → mock; missing weights → stub; bridge stub → synthetic EEG. The privacy banner reflects which path is active.
- At runtime, the supervisor retries live EEG up to 3× with exponential backoff before falling back to synthetic; banner shows "Reconnecting…" + signal-health badge from per-channel RMS
- Experimental, not a production typing path. Track A (jaw clench / blink / double blink / rest, cycling the carousel + dwell-select) is the only supported way to type. Track B (imagined-speech, closed-vocabulary) is a research arm with significant null-result risk —
Sources/NeuralComposeApp/ImaginedSpeechCalibrationView.swiftframes it accordingly in-app. - Hardware ceiling is real, not a tuning problem. The Muse channel set (TP9, AF7, AF8, TP10) has nothing over Broca's area (~F7/F3) or left superior temporal gyrus (~T7/T3) — the regions covert-speech decoding relies on. AF7/AF8 are dominated by the ocular artifacts Track A exploits as a feature; imagined-speech work needs to suppress those instead. No published 4-channel dry-electrode Muse imagined-speech result clears a defensible chance bar.
- Pre-registration gate (must pass before promoting past experimental): a single binary held-out balanced-accuracy threshold — e.g. 65% on a 2-class discrimination, 5-fold within-subject cross-validation, n ≥ 50 trials per class — pinned before training. If it doesn't clear, Track B parks; do not promote on training accuracy or "looks promising."
- Architecture isolation rule: keep Track B's labels, classifier protocol,
.mlpackage, and trainer parallel to Track A's, never extensions of them — mixing imagined-word labels intoCalibrationLabel/IntentClasswould corrupt the production model the next timetrain-intent-classifier.pyruns against a mixed session.
- Lives entirely in
WorldModel/, decoupled from the app on purpose. PyTorch, not MLX; no import ofBCICore/BCIEEG/BCIClassifier/BCILLM. The MLX-isolation rule above doesn't apply here because nothing here touches the SwiftPM target graph. - Synthetic by design, not a shortcut. The motivating idea — a JEPA-style transition predictor over EEG-derived
SpectralState/GenerationAdaptation— is real but currently unbuildable: one processed night of sleep data, zero logged interaction events (InteractionLoggingis opt-in, off by default perADR-005-local-interaction-logging.md). A JEPA needs volume, action variation, and temporal transitions none of that provides yet. So the architecture gets proven on a synthetic 2D continuous-control task first, where ground truth and volume are both free. - Multi-day, staged. Day 1 (synthetic env + trajectory dataset + DataLoader) is the only stage landed so far. See
WorldModel/README.mdfor the full four-day plan and current status — don't assume later days exist without checking. - Pointing this at real EEG data is a future decision, not an assumption. Nothing here should be read as committing to that path; it's contingent on the architecture actually working on the toy task first.
- MLX path requires full Xcode, not just Command Line Tools — SwiftPM needs Xcode to compile mlx-swift's Metal kernels. See
MODEL_SETUP.md. There's a parkedvenv.py314.parkedfrom a Python 3.14 compatibility issue — current venv is regular Python. Info.plistis intentionally not declared as a SwiftPM resource. SwiftPM forbids top-levelInfo.plist.swift runworks fine without it; an Xcode build picks it up.InternalImportsByDefaultis intentionally NOT enabled — the friction with public Foundation-typed signatures wasn't worth it for an executable + 4 internal libraries.- Models are NOT in the repo. Drop
IntentClassifier.mlmodelcor.mlpackageintoModels/; drop an MLX-converted LLM directory underModels/<name>/. Factories auto-detect at launch. - Synthetic mode is the canonical "it works" baseline — if something breaks, fall back to
./Scripts/run-synthetic.shto localize whether it's the pipeline or a real component. venv/here is for the Python training helper only; nothing in the app touches it.