Instructions to use Neurazum/bai-Mind-16P with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Keras
How to use Neurazum/bai-Mind-16P with Keras:
# Available backend options are: "jax", "torch", "tensorflow". import os os.environ["KERAS_BACKEND"] = "jax" import keras model = keras.saving.load_model("hf://Neurazum/bai-Mind-16P") - Notebooks
- Google Colab
- Kaggle
bai-Mind-16P
NOTE! How it works โ it reads your inner speech
This model analyses the words you silently say inside your head. When you imagine saying a direction word โ e.g. "up", "down", "left", "right" โ without moving your lips, tongue or jaw, your brain still produces a measurable EEG pattern for that imagined word. bai-Mind-16P learns and recognises that pattern. It is not reading muscle movement, eye motion, or any external signal โ it decodes the imagined (inner) speech of the word itself. The command you get out is the word you were silently repeating in your mind.
Description
bai-Mind-16P is an EEG inner-speech (imagined speech) brainโcomputer interface model from the bai-Mind family, optimized for 16-electrode OpenBCI setups (Cyton+Daisy). It decodes direction commands โ Up / Down / Left / Right โ that the user silently repeats in their head, and turns them into computer commands.
This is the 16-channel variant of the bai-Mind line: each electrode count is released as its own dedicated model. bai-Mind-16P targets the popular 16-channel OpenBCI montage, which is the practical sweet spot for inner-speech decoding (more channels gave no measurable benefit; far fewer degraded accuracy).
Unlike "plug-and-play" claims, the model is built around per-user calibration: each user records a few minutes of their own labelled EEG and trains a personal model on their own computer (no GPU required โ the network is only ~2โ4K parameters). Subject-independent decoding of inner speech does not work and is not offered; this is consistent with the published literature.
bai-Mind-16P is fully open source and is designed as an honest reference for the real, current limits of inner-speech BCIs โ not as a "mind reading" product.
Audience / Target
bai-Mind models are developed for researchers, universities, BCI/neurotech communities, accessibility projects, makers and students. Not a medical device; not for clinical, diagnostic or safety-critical use.
Architecture
| Input | Encoder | Output |
|---|---|---|
| EEG (16 channels ร 640 samples @256 Hz, 2.5 s inner-speech window) | EEGNet Temporal Conv + Depthwise Spatial Conv + Separable Conv |
โ Softmax โ 2-direction (Up/Down and Left/Right) or 4-direction command |
- Backbone: EEGNet (Lawhern et al. 2018) โ compact CNN for EEG. F1=8 temporal filters, D=2 depth multiplier, F2=16 separable filters, temporal kernel 64, dropout 0.5, max-norm constraints.
- Input: per-epoch, per-channel z-score normalised EEG; 2.5 s action window (1.0โ3.5 s post-cue) resampled to 256 Hz.
- Channel profile (16-ch, OpenBCI Cyton+Daisy):
Fp1, Fp2, F3, F4, C3, C4, P3, P4, O1, O2, F7, F8, T7, T8, P7, P8(10-20 system). Electrodes must be placed in this order and the live signal supplied in the same order. - Parameters: ~2,000โ4,000 โ deliberately tiny to fit small per-user calibration sets and avoid overfitting.
- Training: from scratch per user (within-subject); no transfer / pre-trained weights (subject-independent transfer is chance-level for inner speech).
- Data paradigm: Inner Speech Dataset (Nieto et al. 2021, OpenNeuro
ds003626, CC0) โ 10 subjects, 128-ch BioSemi mapped to the 16 nearest electrodes, 256 Hz.
Other electrode counts are released as separate models (e.g. bai-Mind-8P, bai-Mind-64P, bai-Mind-128P). This card covers the 16-channel model only.
General Tests
Within-subject (per-user calibration). Chance = 50% (2-direction) / 25% (4-direction). Inner-speech BCIs are subject-dependent: performance varies strongly per person ("BCI literacy"). Fill in your measured values.
Results Table
| Task (Up/Down) | Profile | Params | Accuracy | Precision | Recall | F1 | F2 | ROC-AUC | PR-AUC | Specificity | Kappa | MCC | Log-loss | ITR (bit/min) |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| bai-Mind-UD-16P_ft07 (BEST) | OpenBCI-16 (16 Channels) | 2K | %80 | %80 | %80 | %80 | %80 | %83 | %82 | %75 | +0.601 | %60 | %58 | 6.67 |
| bai-Mind-UD-16P_ft09 | OpenBCI-16 (16 Channels) | 2K | %70 | %70 | %70 | %70 | %70 | %76 | %81 | %73 | +0.400 | %40 | %60 | 2.85 |
| bai-Mind-UD-16P_ft02 | OpenBCI-16 (16 Channels) | 2K | %69 | %74 | %70 | %69 | %69 | %72 | %75 | %50 | +0.401 | %44 | %66 | 2.72 |
| bai-Mind-UD-16P_ft05 | OpenBCI-16 (16 Channels) | 2K | %66 | %67 | %66 | %66 | %66 | %75 | %78 | %71 | +0.331 | %33 | %61 | 1.96 |
| bai-Mind-UD-16P_ft04 | OpenBCI-16 (16 Channels) | 2K | %63 | %65 | %63 | %62 | %63 | %68 | %71 | %80 | +0.267 | %28 | %65 | 1.25 |
| bai-Mind-UD-16P_ft03 | OpenBCI-16 (16 Channels) | 2K | %56 | %57 | %57 | %57 | %57 | %56 | %53 | %53 | +0.133 | %13 | %70 | 0.31 |
| bai-Mind-UD-16P_ft06 | OpenBCI-16 (16 Channels) | 2K | %56 | %57 | %57 | %57 | %57 | %61 | %65 | %53 | +0.133 | %13 | %66 | 0.31 |
| bai-Mind-UD-16P_ft01 | OpenBCI-16 (16 Channels) | 2K | %53 | %53 | %53 | %53 | %53 | %55 | %62 | %53 | +0.067 | %7 | %77 | 0.08 |
| bai-Mind-UD-16P_base | OpenBCI-16 (16 Channels) | 2K | %53 | %53 | %53 | %53 | %53 | %53 | %54 | %48 | +0.058 | %6 | %73 | 0.06 |
| bai-Mind-UD-16P_ft08 | OpenBCI-16 (16 Channels) | 2K | %50 | %50 | %50 | %47 | %48 | %52 | %56 | %27 | +0.000 | %0 | %73 | 0.00 |
| bai-Mind-UD-16P_ft00 | OpenBCI-16 (16 Channels) | 2K | %48 | %47 | %47 | %47 | %47 | %60 | %58 | %62 | -0.052 | %-5 | %67 | 0.03 |
| Task (Left/Right) | ||||||||||||||
| bai-Mind-LR-16P_ft08 (BEST) | OpenBCI-16 (16 Channels) | 2K | %60 | %60 | %60 | %60 | %60 | %59 | %63 | %53 | +0.200 | %20 | %68 | 0.70 |
| bai-Mind-LR-16P_ft06 | OpenBCI-16 (16 Channels) | 2K | %57 | %57 | %57 | %57 | %57 | %67 | %69 | %53 | +0.133 | %13 | %69 | 0.31 |
| bai-Mind-LR-16P_ft09 | OpenBCI-16 (16 Channels) | 2K | %57 | %57 | %57 | %55 | %56 | %66 | %73 | %40 | +0.133 | %14 | %69 | 0.31 |
| bai-Mind-LR-16P_ft07 | OpenBCI-16 (16 Channels) | 2K | %56 | %57 | %56 | %56 | %56 | %56 | %56 | %46 | +0.127 | %13 | %69 | 0.25 |
| bai-Mind-LR-16P_ft01 | OpenBCI-16 (16 Channels) | 2K | %53 | %54 | %53 | %52 | %53 | %56 | %60 | %40 | +0.067 | %7 | %69 | 0.08 |
| bai-Mind-LR-16P_ft05 | OpenBCI-16 (16 Channels) | 2K | %52 | %52 | %52 | %52 | %52 | %62 | %64 | %43 | +0.044 | %4 | %69 | 0.02 |
| bai-Mind-LR-16P_base | OpenBCI-16 (16 Channels) | 2K | %48 | %48 | %48 | %48 | %48 | %47 | %49 | %42 | -0.042 | %-4 | %69 | 0.03 |
| bai-Mind-LR-16P_ft04 | OpenBCI-16 (16 Channels) | 2K | %47 | %47 | %47 | %46 | %47 | %54 | %58 | %53 | -0.067 | %-7 | %69 | 0.08 |
| bai-Mind-LR-16P_ft00 | OpenBCI-16 (16 Channels) | 2K | %44 | %44 | %44 | %44 | %44 | %52 | %56 | %38 | -0.115 | %-12 | %69 | 0.25 |
| bai-Mind-LR-16P_ft02 | OpenBCI-16 (16 Channels) | 2K | %43 | %44 | %44 | %43 | %44 | %48 | %49 | %42 | -0.128 | %-13 | %69 | 0.30 |
| bai-Mind-LR-16P_ft03 | OpenBCI-16 (16 Channels) | 2K | %37 | %37 | %37 | %37 | %37 | %40 | %56 | %33 | -0.267 | %-27 | %70 | 1.25 |
| Task (Up/Down/Left/Right) | ||||||||||||||
| bai-Mind-4-16P_ft06 (BEST) | OpenBCI-16 (16 Channels) | 2.7K | %43 | %51 | %43 | %44 | %43 | %63 | N/A | N/A | +0.244 | %26 | %138 | 2.75 |
| bai-Mind-4-16P_ft09 | OpenBCI-16 (16 Channels) | 2.7K | %35 | %34 | %35 | %32 | %33 | %57 | N/A | N/A | +0.133 | %14 | %138 | 0.86 |
| bai-Mind-4-16P_ft08 | OpenBCI-16 (16 Channels) | 2.7K | %31 | %34 | %32 | %32 | %31 | %53 | N/A | N/A | +0.089 | %9 | %138 | 0.39 |
| bai-Mind-4-16P_ft03 | OpenBCI-16 (16 Channels) | 2.7K | %30 | %27 | %30 | %25 | %27 | %59 | N/A | N/A | +0.067 | %7 | %138 | 0.22 |
| bai-Mind-4-16P_ft05 | OpenBCI-16 (16 Channels) | 2.7K | %30 | %27 | %29 | %26 | %27 | %53 | N/A | N/A | +0.060 | %6 | %138 | 0.19 |
| bai-Mind-4-16P_ft04 | OpenBCI-16 (16 Channels) | 2.7K | %28 | %25 | %28 | %25 | %27 | %54 | N/A | N/A | +0.044 | %5 | %138 | 0.10 |
| bai-Mind-4-16P_base | OpenBCI-16 (16 Channels) | 2.7K | %26 | %28 | %27 | %24 | %25 | %54 | N/A | N/A | +0.022 | %2 | %138 | 0.03 |
| bai-Mind-4-16P_ft07 | OpenBCI-16 (16 Channels) | 2.7K | %26 | %25 | %26 | %24 | %25 | %55 | N/A | N/A | +0.012 | %1 | %138 | 0.01 |
| bai-Mind-4-16P_ft01 | OpenBCI-16 (16 Channels) | 2.7K | %25 | %34 | %25 | %24 | %24 | %54 | N/A | N/A | +0.000 | %0 | %138 | 0.00 |
| bai-Mind-4-16P_ft02 | OpenBCI-16 (16 Channels) | 2.7K | %24 | %28 | %25 | %25 | %24 | %45 | N/A | N/A | -0.003 | %-0 | %139 | 0.00 |
| bai-Mind-4-16P_ft-00 | OpenBCI-16 (16 Channels) | 2.7K | %22 | %22 | %22 | %21 | %22 | %46 | N/A | N/A | -0.041 | %-4 | %139 | 0.09 |
*Since PR-AUC and specificity are binary classification metrics, these values are not available for 4-class models.
*Subject-independent ("plug-and-play") models are chance-level and not recommended.
*Results depend on per-user calibration quality, electrode placement and device.
*No transfer learning or pre-trained weights were used.
Usage
Python Script โ Streamlit App
"""
==============================================================================
bai-Mind-16P - Inner-Speech Direction BCI (standalone Streamlit app)
==============================================================================
Single-file, shareable interface. No other project files required.
Optimized for the 16-channel OpenBCI (Cyton+Daisy) montage.
What it does:
- Load a trained model (.h5 + its .meta.json)
- LIVE mode: stream EEG from a BrainFlow device (OpenBCI or the built-in
SYNTHETIC board for hardware-free testing) and predict the imagined
direction in real time, shown on an arrow board.
- CALIBRATION mode: record your own labelled trials, train a personal model
on your own machine (CPU is fine, ~3-4K params), and get an honest
"did it work for you?" verdict (kappa threshold).
Architecture embedded below (EEGNet) so checkpoints load standalone.
Run:
pip install streamlit tensorflow brainflow numpy scipy scikit-learn
streamlit run bai-Mind-16P.py
==============================================================================
"""
import json, time, datetime
from pathlib import Path
import numpy as np
import streamlit as st
# ---- Signal / paradigm constants -------------------------------------------
SFREQ = 256.0 # Hz
ACTION_TMIN = 1.0 # s (inner-speech window start, post-cue)
ACTION_TMAX = 3.5 # s (window end) -> 2.5 s -> 640 samples
N_SAMPLES = int(SFREQ * (ACTION_TMAX - ACTION_TMIN))
ARROW = {"Up": "โฌ๏ธ", "Down": "โฌ๏ธ", "Left": "โฌ
๏ธ", "Right": "โก๏ธ"}
GRID_POS = {"Up": (0, 1), "Left": (1, 0), "Right": (1, 2), "Down": (2, 1)}
# 16-channel OpenBCI (Cyton+Daisy) montage โ this model's fixed channel set
DEVICE_MONTAGES = {
"openbci16": ["Fp1","Fp2","F3","F4","C3","C4","P3","P4",
"O1","O2","F7","F8","T7","T8","P7","P8"],
}
st.set_page_config(page_title="bai-Mind-16P", page_icon="๐ง ", layout="wide")
# ============================================================================
# MODEL ARCHITECTURE (EEGNet) โ must match the trained model for weight load
# ============================================================================
def build_eegnet(n_channels, n_samples, n_classes,
F1=8, D=2, F2=16, kernel_length=64, dropout=0.5):
import tensorflow as tf
from tensorflow.keras import layers, models, constraints
kern = min(kernel_length, n_samples)
inp = layers.Input(shape=(n_channels, n_samples, 1), name="eeg")
x = layers.Conv2D(F1, (1, kern), padding="same", use_bias=False)(inp)
x = layers.BatchNormalization()(x)
x = layers.DepthwiseConv2D((n_channels, 1), use_bias=False, depth_multiplier=D,
depthwise_constraint=constraints.max_norm(1.0))(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("elu")(x)
x = layers.AveragePooling2D((1, 4))(x)
x = layers.Dropout(dropout)(x)
x = layers.SeparableConv2D(F2, (1, 16), padding="same", use_bias=False)(x)
x = layers.BatchNormalization()(x)
x = layers.Activation("elu")(x)
x = layers.AveragePooling2D((1, 8))(x)
x = layers.Dropout(dropout)(x)
x = layers.Flatten()(x)
out = layers.Dense(n_classes, kernel_constraint=constraints.max_norm(0.25))(x)
out = layers.Activation("softmax")(out)
return models.Model(inp, out, name="bai_mind_16p")
@st.cache_resource(show_spinner="Loading model...")
def load_model(h5_path):
import tensorflow as tf
meta = json.load(open(Path(h5_path).with_suffix(".meta.json"), encoding="utf-8"))
try:
model = tf.keras.models.load_model(h5_path)
except Exception: # version skew -> rebuild + load weights
model = build_eegnet(int(meta["n_channels"]), N_SAMPLES, len(meta["labels"]))
model.load_weights(h5_path)
return model, meta
def predict(model, meta, eeg):
"""eeg: (C, T) in meta['channels'] order -> (label, {label: prob})."""
eeg = np.asarray(eeg, np.float32)
T = eeg.shape[1]
eeg = eeg[:, :N_SAMPLES] if T >= N_SAMPLES else np.pad(eeg, ((0, 0), (0, N_SAMPLES - T)))
mu = eeg.mean(1, keepdims=True); sd = eeg.std(1, keepdims=True) + 1e-7
x = ((eeg - mu) / sd)[np.newaxis, ..., np.newaxis]
p = model.predict(x, verbose=0)[0]
labels = meta["labels"]
return labels[int(p.argmax())], {l: float(v) for l, v in zip(labels, p)}
# ============================================================================
# VISUALS
# ============================================================================
def render_directions(labels, pred=None, proba=None):
st.markdown("#### Direction Board")
for r in range(3):
cols = st.columns(3)
for c in range(3):
cell = next((l for l, pos in GRID_POS.items() if pos == (r, c) and l in labels), None)
with cols[c]:
if cell is None:
if (r, c) == (1, 1):
st.markdown("<div style='text-align:center;font-size:40px'>๐ง </div>",
unsafe_allow_html=True)
continue
conf = proba.get(cell, 0.0) if proba else 0.0
bg = "#1f9d55" if cell == pred else "#2b2b2b"
st.markdown(
f"<div style='text-align:center;padding:14px;border-radius:12px;"
f"background:{bg};border:1px solid #444'>"
f"<div style='font-size:40px'>{ARROW[cell]}</div>"
f"<div style='color:#ddd'>{cell}</div>"
f"<div style='color:#aaa;font-size:13px'>{conf*100:.0f}%</div></div>",
unsafe_allow_html=True)
if proba:
st.bar_chart({l: [proba[l]] for l in labels})
# ============================================================================
# LIVE PREDICTION (BrainFlow)
# ============================================================================
def live_mode(model, meta):
try:
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds
except ImportError:
st.error("BrainFlow required: pip install brainflow"); return
boards = {"Synthetic (no hardware)": BoardIds.SYNTHETIC_BOARD,
"OpenBCI Cyton+Daisy (16ch)": BoardIds.CYTON_DAISY_BOARD,
"OpenBCI Cyton (8ch)": BoardIds.CYTON_BOARD}
c1, c2 = st.columns([1, 2])
with c1:
bname = st.selectbox("Device", list(boards), index=0)
serial = st.text_input("Serial port / MAC (empty for Synthetic)", "")
st.caption(f"Model expects {meta['n_channels']} channels in this order: "
f"{', '.join(meta['channels'])}")
if "board" not in st.session_state: st.session_state.board = None
a, b = st.columns(2)
if a.button("โถ๏ธ Start", use_container_width=True) and st.session_state.board is None:
p = BrainFlowInputParams(); p.serial_port = serial
bd = BoardShim(boards[bname], p)
try:
bd.prepare_session(); bd.start_stream()
st.session_state.board = bd; st.session_state.bid = boards[bname]
st.success("Streaming.")
except Exception as e:
st.error(f"Connect failed: {e}")
if b.button("โน๏ธ Stop", use_container_width=True) and st.session_state.board:
try: st.session_state.board.stop_stream(); st.session_state.board.release_session()
except Exception: pass
st.session_state.board = None
snap = st.button("๐ฎ Predict", type="primary", use_container_width=True)
with c2:
if snap and st.session_state.board:
bd, bid = st.session_state.board, st.session_state.bid
sr = BoardShim.get_sampling_rate(bid)
rows = BoardShim.get_eeg_channels(bid)[:meta["n_channels"]]
raw = bd.get_current_board_data(int(sr * 3))
if raw.shape[1] < 10 or len(rows) < meta["n_channels"]:
st.warning("Not enough data / channels.")
else:
sig = raw[rows, :]
if abs(sr - SFREQ) > 1:
import scipy.signal as ss
sig = ss.resample(sig, int(sig.shape[1] * SFREQ / sr), axis=1)
sig = sig[:, -N_SAMPLES:]
pred, proba = predict(model, meta, sig)
st.markdown(f"### Prediction: {ARROW.get(pred,'')} {pred}")
render_directions(meta["labels"], pred, proba)
else:
render_directions(meta["labels"])
# ============================================================================
# CALIBRATION (record + train personal model + kappa verdict)
# ============================================================================
def record_session(board, bid, n_channels, labels, trials_per_class, ui):
from brainflow.board_shim import BoardShim
sr = BoardShim.get_sampling_rate(bid)
rows = BoardShim.get_eeg_channels(bid)[:n_channels]
action = ACTION_TMAX - ACTION_TMIN
order = [c for c in range(len(labels)) for _ in range(trials_per_class)]
np.random.RandomState(0).shuffle(order)
X, y, tot = [], [], len(order)
for i, ci in enumerate(order):
ui("rest", labels[ci], i, tot); time.sleep(1.5)
ui("cue", labels[ci], i, tot); time.sleep(1.0)
board.get_board_data()
ui("think", labels[ci], i, tot); time.sleep(action + 0.2)
raw = board.get_board_data()
if raw.shape[1] < 5: continue
sig = raw[rows, :].astype(np.float32)
if abs(sr - SFREQ) > 1:
import scipy.signal as ss
sig = ss.resample(sig, int(sig.shape[1] * SFREQ / sr), axis=1)
sig = sig[:, :N_SAMPLES] if sig.shape[1] >= N_SAMPLES else \
np.pad(sig, ((0, 0), (0, N_SAMPLES - sig.shape[1])))
X.append(sig); y.append(ci)
ui("done", "", tot, tot)
return np.asarray(X, np.float32), np.asarray(y, int)
def train_personal(X, y, labels, profile, channels, user, out_dir):
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, cohen_kappa_score
Xn = ((X - X.mean(2, keepdims=True)) / (X.std(2, keepdims=True) + 1e-7))[..., np.newaxis]
n_ch, n_t, n_cls = Xn.shape[1], Xn.shape[2], len(labels)
Xtr, Xte, ytr, yte = train_test_split(Xn, y, test_size=0.25, random_state=42, stratify=y)
Xtr, Xva, ytr, yva = train_test_split(Xtr, ytr, test_size=0.18, random_state=42, stratify=ytr)
model = build_eegnet(n_ch, n_t, n_cls)
model.compile(optimizer=tf.keras.optimizers.Adam(1e-3),
loss="sparse_categorical_crossentropy", metrics=["accuracy"])
model.fit(Xtr, ytr, validation_data=(Xva, yva), epochs=120, batch_size=32, verbose=0,
callbacks=[tf.keras.callbacks.EarlyStopping(monitor="val_accuracy", mode="max",
patience=25, restore_best_weights=True)])
yp = model.predict(Xte, verbose=0).argmax(1)
m = {"accuracy": float(accuracy_score(yte, yp)),
"kappa": float(cohen_kappa_score(yte, yp)),
"chance": 1.0 / n_cls, "n_test": int(len(yte))}
name = f"user_{user}_{len(labels)}cls_{profile}"
h5 = Path(out_dir) / f"{name}.h5"; model.save(h5)
json.dump({"model_name": "bai-Mind-16P (personal)", "labels": list(labels),
"channels": list(channels), "n_channels": int(n_ch),
"profile": profile, "mode": "user_calibration", "metrics": m,
"created": datetime.datetime.now().isoformat(timespec="seconds")},
open(h5.with_suffix(".meta.json"), "w", encoding="utf-8"),
ensure_ascii=False, indent=2)
return str(h5), m
def verdict(m):
k = m["kappa"]
if k >= 0.25:
st.success(f"โ
Works well for you โ kappa {k:+.2f}, acc %{m['accuracy']*100:.0f}.")
elif k >= 0.10:
st.warning(f"๐ก Weak โ kappa {k:+.2f}. Record more trials; keep the same word/rhythm; "
f"avoid blinks/movement during the think window.")
else:
st.error(f"๐ด Not working yet โ kappa {k:+.2f} โ chance. This is normal for some users "
f"(BCI literacy). Re-read the guide, try 2 directions, record more trials.")
if m["n_test"] < 20:
st.caption(f"โ ๏ธ Small test set ({m['n_test']} trials) โ score is noisy.")
def calibration_mode(out_dir):
st.header("๐ฏ Calibration โ train your own model")
try:
from brainflow.board_shim import BoardShim, BrainFlowInputParams, BoardIds
except ImportError:
st.error("BrainFlow required: pip install brainflow"); return
boards = {"Synthetic (no hardware)": BoardIds.SYNTHETIC_BOARD,
"OpenBCI Cyton+Daisy (16ch)": BoardIds.CYTON_DAISY_BOARD}
c1, c2 = st.columns(2)
with c1:
user = st.text_input("User name", "user1")
task = st.selectbox("Task", ["2-direction", "4-direction"])
labels = ["Up", "Down"] if task == "2-direction" else ["Up", "Down", "Left", "Right"]
profile = "openbci16"
st.caption("Profile: openbci16 (16 channels) โ fixed for bai-Mind-16P.")
tpc = st.slider("Trials per class", 15, 60, 35, 5)
with c2:
bname = st.selectbox("Device", list(boards), index=0)
serial = st.text_input("Serial / MAC (empty for Synthetic)", "")
chans = DEVICE_MONTAGES[profile]
st.info(f"**{len(chans)} channels:** {', '.join(chans)}\n\n"
f"Total {tpc*len(labels)} trials, ~{tpc*len(labels)*5//60} min.")
box, prog = st.empty(), st.empty()
if st.button("โบ๏ธ Start recording", type="primary", use_container_width=True):
p = BrainFlowInputParams(); p.serial_port = serial
bd = BoardShim(boards[bname], p)
try: bd.prepare_session(); bd.start_stream()
except Exception as e: st.error(f"Connect failed: {e}"); return
names = {"rest": "๐ Rest", "cue": "๐ Get ready", "think": "๐ง THINK NOW", "done": "โ
Done"}
def ui(ph, lbl, i, tot):
col = "#1f9d55" if ph == "think" else "#444"
box.markdown(f"<div style='text-align:center;padding:24px;border-radius:14px;"
f"background:{col}'><div style='font-size:64px'>{ARROW.get(lbl,'')}</div>"
f"<div style='color:#fff;font-size:22px'>{names.get(ph,'')} โ {lbl}</div></div>",
unsafe_allow_html=True)
prog.progress(min(i/max(tot,1), 1.0), text=f"Trial {i}/{tot}")
with st.spinner("Recording โ think the shown direction..."):
X, y = record_session(bd, boards[bname], len(chans), labels, tpc, ui)
try: bd.stop_stream(); bd.release_session()
except Exception: pass
st.session_state.cal = (X, y, labels, profile, chans, user)
st.success(f"{len(y)} trials recorded. Now train.")
if "cal" in st.session_state and st.button("๐ง Train model", use_container_width=True):
X, y, labels, profile, chans, user = st.session_state.cal
with st.spinner("Training on CPU (seconds)..."):
h5, m = train_personal(X, y, labels, profile, chans, user, out_dir)
st.success(f"Saved: {Path(h5).name}")
a, b, c = st.columns(3)
a.metric("Accuracy", f"{m['accuracy']*100:.1f}%", f"chance {m['chance']*100:.0f}%")
b.metric("Kappa", f"{m['kappa']:+.3f}"); c.metric("Test trials", m["n_test"])
verdict(m)
# ============================================================================
# MAIN
# ============================================================================
def main():
st.title("๐ง bai-Mind-16P โ Inner-Speech Direction BCI")
st.caption("EEGNet ยท 16-ch OpenBCI ยท per-user calibration ยท open source ยท CC-BY-NC-SA 4.0")
mode = st.sidebar.radio("Mode", ["โถ๏ธ Use (live)", "๐ฏ Calibration"])
out_dir = st.sidebar.text_input("Model folder", "outputs")
Path(out_dir).mkdir(exist_ok=True)
if mode == "๐ฏ Calibration":
calibration_mode(out_dir); return
h5s = sorted(Path(out_dir).glob("*.h5"))
if not h5s:
st.warning(f"No model in '{out_dir}'. Use Calibration mode to train one, "
f"or copy a .h5 + .meta.json there."); st.stop()
names = [p.name for p in h5s]
default = next((i for i, n in enumerate(names) if n.startswith("user_")), 0)
sel = st.sidebar.selectbox("Model", names, index=default)
model, meta = load_model(str(Path(out_dir) / sel))
st.sidebar.success(f"{len(meta['labels'])} directions: {', '.join(meta['labels'])}")
st.sidebar.write(f"Profile: {meta.get('profile','?')} ยท {meta['n_channels']} ch")
if "metrics" in meta:
st.sidebar.metric("Reported accuracy",
f"{meta['metrics']['accuracy']*100:.1f}%")
live_mode(model, meta)
st.divider()
st.caption("Subject-independent use is chance-level; use a personally calibrated "
"(user_) model. Real-device accuracy is lower than research data.")
if __name__ == "__main__":
main()
Requirements
- Python โฅ 3.9
- TensorFlow โฅ 2.12 (Keras)
- streamlit, brainflow, numpy, scipy, scikit-learn
- No GPU required โ the model is ~3โ4K parameters and trains on CPU in seconds. A 16-channel OpenBCI (Cyton+Daisy) device is needed (BUT NOT REQUIRED) for live use; a hardware-free Synthetic board is built in for testing.
License
CC-BY-NC-SA 4.0 โ see LICENSE for details. Training data (Inner Speech Dataset, Nieto et al. 2021) is licensed separately (CC0).
Support
- Website: Neurazum
- Email: contact@neurazum.com
- Downloads last month
- -