You need to agree to share your contact information to access this model

This repository is publicly accessible, but you have to accept the conditions to access its files and content.

Log in or Sign Up to review the conditions and access this model content.

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


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