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 4.0 โ€” see LICENSE for details. Training data (Inner Speech Dataset, Nieto et al. 2021) is licensed separately (CC0).

Support


Downloads last month
-
Inference Providers NEW
This model isn't deployed by any Inference Provider. ๐Ÿ™‹ Ask for provider support