SPX Benchmarks Platform Docs API → GitHub →

API Reference

Auto-generated from docstrings across 63 modules. Regenerate with python tools/build_api_page.py.

subvocal.exceptions

Typed exception hierarchy for the Subvocal SDK.

Every error the SDK raises derives from :class:`SubvocalError`, so integrators can catch one base class at the application boundary::

try: pipeline.step() except SubvocalError as e: logger.error("subvocal failure: %s", e)

Subclasses also inherit the builtin exception type the SDK historically raised in each situation (``RuntimeError``, ``ImportError``, ...), so pre-existing ``except RuntimeError`` handlers keep working across upgrades.

class SubvocalError(Exception)

Base class for all errors raised by the Subvocal SDK.

class ConfigurationError(SubvocalError, ValueError)

Invalid or missing configuration (API keys, parameters, paths).

class HardwareError(SubvocalError, RuntimeError)

A hardware source failed: not connected, stream fault, or parse error.

class MissingDependencyError(SubvocalError, ImportError)

An optional dependency is required for the requested feature.

The message always names the pip extra that provides it.

class ProviderError(SubvocalError, RuntimeError)

An LLM provider call failed after exhausting retries.

class DecodingError(SubvocalError, RuntimeError)

Shorthand-to-intent decoding produced no usable result.

class PolicyViolationError(SubvocalError, PermissionError)

An action was rejected by the authorization policy engine.

Raised only when the pipeline is configured with ``raise_on_policy_violation=True``; by default rejections are traced and reported through callbacks without raising.

class CalibrationError(SubvocalError, RuntimeError)

Per-user classifier calibration or model loading failed.

subvocal.paths

Resolution of writable data and model directories.

The SDK writes pipeline traces, correction logs, and trained model weights at runtime. After installation the package directory lives in ``site-packages`` and must never be written to, so all writable paths resolve to a per-user data directory instead. Resolution order:

1. Environment variables ``SUBVOCAL_DATA_DIR`` / ``SUBVOCAL_MODELS_DIR``. 2. The OS-conventional user data directory (via ``platformdirs``), e.g. ``~/Library/Application Support/subvocal`` on macOS or ``~/.local/share/subvocal`` on Linux.

Directories are created on first use.

get_data_dir

def get_data_dir() -> str

Writable directory for traces, correction logs, and datasets.

get_models_dir

def get_models_dir() -> str

Writable directory for trained and downloaded model weights.

subvocal.config

class HardwareConfig(BaseModel)

No description.

class DSPConfig(BaseModel)

No description.

class ClassifierConfig(BaseModel)

No description.

class ProviderConfig(BaseModel)

No description.

class PolicyConfig(BaseModel)

No description.

class TelemetryConfig(BaseModel)

No description.

class RuntimeConfig(BaseModel)

No description.

class AuthConfig(BaseModel)

No description.

class SubvocalConfig(BaseModel)

No description.

merge_env_overrides

def merge_env_overrides(config_dict: dict[str, Any]) -> dict[str, Any]

Inspects environment variables for SUBVOCAL_ keys and merges them into the config dict.

Only variables that use the ``__`` section separator (e.g. ``SUBVOCAL_HARDWARE__SAMPLE_RATE``) are treated as config overrides, since every configurable field lives inside a named section. Flat variables such as ``SUBVOCAL_DATA_DIR`` / ``SUBVOCAL_MODELS_DIR`` are reserved for :mod:`subvocal.paths` and are ignored here.

load_config

def load_config(path: str | None = None) -> SubvocalConfig

Loads a SubvocalConfig from a YAML file (if provided) and merges environment overrides.

subvocal.core.corrections

Correction-capture loop and fine-tuning hook implementation for Subvocal SDK.

class CorrectionLogEntry(BaseModel)

Represents a single correction entry logged when a user fixes an incorrect reconstruction.

class CorrectionManager

Manages local storage and retrieval of user correction log entries.

__init__

def __init__(self, log_path: str | None = None)

Initializes the manager.

Args: log_path: Path to the JSONL log file. Defaults to 'sdk/data/corrections_log.jsonl'.

log_correction

def log_correction(self, raw_shorthand: str, decoded_intent: str, corrected_intent: str, context: UserContext) -> CorrectionLogEntry

Logs a new correction entry to the local JSONL file.

get_corrections

def get_corrections(self) -> list[CorrectionLogEntry]

Retrieves all logged corrections from the local file.

clear_logs

def clear_logs(self) -> None

Clears all logged corrections in the file.

class FinetuningHook

Converts correction logs into training datasets for fine-tuning LLMs.

export_to_openai

def export_to_openai(entries: list[CorrectionLogEntry], system_instruction: str = 'Translate silent speech shorthand and context into correct system actions.') -> list[dict[str, Any]]

Converts entries to OpenAI chat fine-tuning format.

Format: {"messages": [{"role": "system", "content": ...}, {"role": "user", "content": ...}, {"role": "assistant", "content": ...}]}

export_to_gemini

def export_to_gemini(entries: list[CorrectionLogEntry], system_instruction: str = 'Translate silent speech shorthand and context into correct system actions.') -> list[dict[str, Any]]

Converts entries to Google Gemini fine-tuning format.

Format: {"contents": [...], "systemInstruction": {...}}

export_to_jsonl

def export_to_jsonl(data: list[dict[str, Any]], output_path: str) -> int

Writes the exported dataset list to a JSONL file.

subvocal.core.interfaces

Abstract base interfaces for subvocal middleware components.

Enables pluggable integration of various: - Hardware sources (real-time stream, replay file, synthetic generators). - LLM providers (Gemini, Claude, GPT, local Llama). - Action executors (browser control, device APIs, notifications). - Context providers (OS, active application, contacts, location).

class HardwareSource(ABC)

Abstract interface for subvocal/sEMG sensor data acquisition.

start

def start(self) -> None

Initialize connection and start streaming raw sEMG data.

stop

def stop(self) -> None

Stop data acquisition and disconnect hardware clean.

read_frame

def read_frame(self, window_ms: int) -> Frame

Reads a buffered window of raw sEMG samples.

Args: window_ms: Time duration in milliseconds to buffer and retrieve.

Returns: A Frame containing the Sample points.

is_connected

def is_connected(self) -> bool

Returns True if the sensor connection is healthy.

class LLMProvider(ABC)

Abstract interface for turning noisy classified shorthand into semantic Intents.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

Resolves a noisy shorthand token stream to a structured Intent.

Args: tokens: A sequence of classified CommandTokens. context: The UserContext state snapshot at execution time.

Returns: The reconstructed Intent.

get_provider_name

def get_provider_name(self) -> str

Returns the name of the LLM provider (e.g., 'gemini', 'anthropic').

class ActionExecutor(ABC)

Abstract interface for dispatching resolved agentic Actions.

execute

def execute(self, action: Action) -> Any

Dispatches the action to device APIs or agent tools.

Args: action: The Action object to execute.

Returns: The result of the execution.

can_execute

def can_execute(self, action: Action) -> bool

Returns True if the executor supports this specific action type.

class ContextProvider(ABC)

Abstract interface for retrieving live device or environment context.

get_context

def get_context(self) -> UserContext

Retrieves a snapshot of the active user/system context.

Returns: The UserContext populated with active state metadata.

get_provider_name

def get_provider_name(self) -> str

Returns the name/identifier of this context provider.

class Classifier(ABC)

Abstract interface for classifying physiological raw signals into command tokens.

predict

def predict(self, frame: Frame | Any) -> CommandToken | None

Classifies a Frame of raw signals into a CommandToken (applies gating/cooldown if configured).

predict_raw

def predict_raw(self, frame: Frame | Any) -> tuple[str, float, list[float]]

Predicts the probability distribution for a Frame of raw signals.

Returns: (predicted_class_label, max_probability, all_probabilities_list)

labels

def labels(self) -> list[str]

Returns the list of output labels/classes supported by the classifier.

subvocal.core.llm_providers

Concrete implementations of the LLMProvider interface using urllib.

class BaseLLMProvider(LLMProvider)

Base provider containing prompt formatting and request helpers.

__init__

def __init__(self, api_key: str | None = None, model_name: str | None = None, mock_response: str | None = None, prompt_version: str = 'v1', timeout: float = 10.0, max_retries: int = 2, backoff_seconds: float = 0.5)

Initializes the base LLM provider.

Args: api_key: API authorization key. Resolves from environment if None. model_name: Name of the LLM model to request. mock_response: Optional mock string response to bypass HTTP calls (for testing). prompt_version: The version string of the prompt template to use. timeout: Per-request HTTP timeout in seconds. max_retries: Additional attempts after the first failure for transient errors (connection failures and retryable HTTP statuses). backoff_seconds: Base delay for exponential backoff between retries.

class ClaudeProvider(BaseLLMProvider)

Anthropic Claude LLM provider client.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

class OpenAIProvider(BaseLLMProvider)

OpenAI GPT completions provider client.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

class GeminiProvider(BaseLLMProvider)

Google Gemini content generation provider client.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

class LlamaProvider(BaseLLMProvider)

Local Llama provider client (running via Ollama).

__init__

def __init__(self, ollama_host: str | None = None, model_name: str | None = None, mock_response: str | None = None, prompt_version: str = 'v1')

Initializes the Ollama Llama provider client.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

class HeuristicProvider(LLMProvider)

Fully offline intent reconstruction using the articulatory-distance decoder.

Resolves shorthand phrases against the command vocabulary and the active user context with no network access or API keys. Useful as a development default, an air-gapped deployment mode, and a graceful fallback when no LLM credentials are configured (see :func:`resolve_provider`).

__init__

def __init__(self, min_confidence: float = 0.0)

Initializes the heuristic provider.

Args: min_confidence: Confidence floor below which the intent command is reported as UNKNOWN rather than a low-quality guess.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

resolve_provider

def resolve_provider(prefer: str | None = None, **kwargs) -> LLMProvider

Returns the best available LLM provider for this environment.

Selection order: an explicit ``prefer`` name ("anthropic", "openai", "gemini", "ollama", "heuristic"), then the first provider whose API key is present in the environment, then the offline :class:`HeuristicProvider`.

Args: prefer: Optional provider name to force. **kwargs: Passed through to the chosen provider constructor.

subvocal.core.models

Core data models representing the flow from raw physiological signals to actions.

Defines: - Sample: A single point-in-time multi-channel sensor reading. - Frame: A window of Samples buffered for classifier consumption. - CommandToken: A token/shorthand output by a classifier. - Intent: Reconstructed semantic command and arguments. - Action: Executable instruction dispatched to an executor.

class Sample(BaseModel)

A single multichannel biometric time-series sample.

validate_channels

def validate_channels(cls, v: list[float]) -> list[float]

No description.

class Frame(BaseModel)

A window/segment of Samples buffered for signal processing or ML inference.

validate_frame_ordering

def validate_frame_ordering(self) -> 'Frame'

No description.

to_numpy

def to_numpy(self) -> np.ndarray

Converts the frame samples into a 2D NumPy array.

Returns: np.ndarray of shape (num_samples, num_channels)

class CommandToken(BaseModel)

A token predicted by the raw classifier with associated confidence.

validate_confidence

def validate_confidence(cls, v: float) -> float

No description.

class Intent(BaseModel)

Semantic intent reconstructed from a token stream and active context.

validate_confidence

def validate_confidence(cls, v: float) -> float

No description.

class Action(BaseModel)

The executable instruction dispatched to the system or agent executor.

subvocal.core.pipeline

SubvocalPipeline class coordinating the hardware, classification, and execution layers.

class PipelineStats

Running counters for a pipeline instance, exposed as ``pipeline.stats``.

as_dict

def as_dict(self) -> dict[str, Any]

Returns the counters as a plain dictionary (for logging/export).

class SubvocalPipeline

Orchestrates hardware ingestion, classifier decoding, intent reconstruction, and action execution.

__init__

def __init__(self, hardware: HardwareSource, classify_fn: Callable[[Frame], CommandToken | None], llm_provider: LLMProvider, context_provider: ContextProvider, executor: ActionExecutor, phrase_timeout_seconds: float = 1.5, policy_engine: Any | None = None, dry_run: bool = False, trace_path: str | None = None, raise_on_policy_violation: bool = False, on_token: Callable[[CommandToken], None] | None = None, on_intent: Callable[[Intent], None] | None = None, on_action: Callable[[Action, str], None] | None = None, on_error: Callable[[Exception], None] | None = None, telemetry_service: Any | None = None)

No description.

trace_path

def trace_path(self) -> str | None

No description.

trace_path

def trace_path(self, val: str | None) -> None

No description.

hardware

def hardware(self) -> HardwareSource

No description.

hardware

def hardware(self, val: HardwareSource) -> None

No description.

classify_fn

def classify_fn(self) -> Callable[[Frame], CommandToken | None]

No description.

classify_fn

def classify_fn(self, val: Callable[[Frame], CommandToken | None]) -> None

No description.

llm_provider

def llm_provider(self) -> LLMProvider

No description.

llm_provider

def llm_provider(self, val: LLMProvider) -> None

No description.

context_provider

def context_provider(self) -> ContextProvider

No description.

context_provider

def context_provider(self, val: ContextProvider) -> None

No description.

executor

def executor(self) -> ActionExecutor

No description.

executor

def executor(self, val: ActionExecutor) -> None

No description.

policy_engine

def policy_engine(self) -> Any

No description.

policy_engine

def policy_engine(self, val: Any) -> None

No description.

phrase_timeout_seconds

def phrase_timeout_seconds(self) -> float

No description.

phrase_timeout_seconds

def phrase_timeout_seconds(self, val: float) -> None

No description.

dry_run

def dry_run(self) -> bool

No description.

dry_run

def dry_run(self, val: bool) -> None

No description.

raise_on_policy_violation

def raise_on_policy_violation(self) -> bool

No description.

raise_on_policy_violation

def raise_on_policy_violation(self, val: bool) -> None

No description.

token_buffer

def token_buffer(self) -> list[CommandToken]

Returns the current accumulated tokens in the buffer.

inject_token

def inject_token(self, token: CommandToken) -> None

Thread-safe injection of a token into the buffer.

Acquires self._lock and the underlying session lock to avoid races with Session._process_frame which mutates the same buffer on the OpsQueue thread.

clear_buffer

def clear_buffer(self) -> None

Clears the accumulated command token buffer.

step

def step(self, window_ms: int = 100) -> Action | None

Performs a single step of the pipeline.

Args: window_ms: Buffer window duration in milliseconds to acquire.

Returns: An executed Action if an intent was resolved and dispatched, otherwise None.

process_phrase

def process_phrase(self) -> Action | None

Forces immediate decoding and execution of the accumulated tokens.

Returns: The executed Action if successful, otherwise None.

subvocal.core.prompts

Prompt templates and version manager for intent reconstruction.

class PromptManager

Manages versioned prompt templates for LLM intent decoding.

__init__

def __init__(self, default_version: str = 'v1')

No description.

register_template

def register_template(self, version: str, template: str) -> None

Register a new custom prompt template version.

get_template

def get_template(self, version: str | None = None) -> str

Retrieve a raw prompt template string by version.

format_prompt

def format_prompt(self, noisy_input: str, heuristic_recommendation: str, web_context: str, calendar: str, contacts: str, history: str, version: str | None = None) -> str

Formats the selected prompt template with active inputs.

subvocal.core.security

Security policies and authorization gating for subvocal actions.

class AuthorizationPolicy(ABC)

Abstract base class for all subvocal execution policies.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

Evaluate if the given action is authorized to execute.

Args: action: The proposed Action object. context: The UserContext state snapshot at proposal time.

Returns: True if authorized, False otherwise.

class ConfidenceThresholdPolicy(AuthorizationPolicy)

Restricts execution of actions if classifier/reconstruction confidence is too low.

__init__

def __init__(self, threshold: float = 0.8)

No description.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

No description.

class CommandWhitelistPolicy(AuthorizationPolicy)

Restricts commands to a strict whitelist of allowed actions.

__init__

def __init__(self, allowed_commands: list[str])

No description.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

No description.

class ContextBoundPolicy(AuthorizationPolicy)

Blocks execution of sensitive commands unless the active application is safe.

__init__

def __init__(self, sensitive_commands: list[str], safe_applications: list[str])

No description.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

No description.

class PolicyEngine

Orchestrates authorization consensus across multiple active policies.

__init__

def __init__(self, policies: list[AuthorizationPolicy] | None = None)

No description.

add_policy

def add_policy(self, policy: AuthorizationPolicy) -> None

Add a new policy to the engine.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

Returns True only if all configured policies approve the action.

subvocal.core.testing

Shared test fixtures for Subvocal SDK unit tests.

class MockHardwareSource(HardwareSource)

Generates continuous synthetic sEMG samples (sine + noise).

__init__

def __init__(self, fs: float = 1000.0, num_channels: int = 4)

No description.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

is_connected

def is_connected(self) -> bool

No description.

class MockLLMProvider(LLMProvider)

Maps shorthand token text patterns to GOTO or CLICK intents.

reconstruct_intent

def reconstruct_intent(self, tokens: list[CommandToken], context: UserContext) -> Intent

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

class MockActionExecutor(ActionExecutor)

Records dispatched actions for assertion in tests.

__init__

def __init__(self)

No description.

execute

def execute(self, action: Action) -> Any

No description.

can_execute

def can_execute(self, action: Action) -> bool

No description.

class MockContextProvider(ContextProvider)

Returns a static browser UserContext snapshot.

get_context

def get_context(self) -> UserContext

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

subvocal.context.manager

Context Manager for retrieving, filtering, and searching user context.

Integrates context querying with custom articulatory phonetic distance matching to enable context-aware silent speech decoding.

class ContextManager

Manages active UserContext and provides phonetic searching utilities.

__init__

def __init__(self, context: UserContext)

No description.

update_context

def update_context(self, new_context: UserContext)

Update the active user context state.

get_contact_names

def get_contact_names(self) -> list[str]

Get list of all contact names in plain text.

get_contact_shorthands

def get_contact_shorthands(self) -> list[str]

Get list of all pre-computed contact name shorthands.

get_calendar_titles

def get_calendar_titles(self) -> list[str]

Get list of all calendar event titles in plain text.

get_calendar_shorthands

def get_calendar_shorthands(self) -> list[str]

Get list of all calendar event title shorthands.

get_visible_element_labels

def get_visible_element_labels(self) -> list[str]

Get list of all visible UI element labels in plain text.

get_visible_element_shorthands

def get_visible_element_shorthands(self) -> list[str]

Get list of all visible UI element shorthands.

get_all_context_words

def get_all_context_words(self) -> list[str]

Compile a list of all potential word tokens present in the active context.

Useful for seeding the dictionary candidate generator in the decoder.

search_contacts

def search_contacts(self, noisy_shorthand: str) -> list[tuple[Contact, float]]

Search contacts by comparing noisy shorthand against pre-computed contact shorthands.

Returns: List of tuples (Contact, articulatory_distance) sorted by distance.

search_elements

def search_elements(self, noisy_shorthand: str) -> list[tuple[UIElement, float]]

Search visible UI elements using articulatory distance matching.

Returns: List of tuples (UIElement, articulatory_distance) sorted by distance.

subvocal.context.mock_data

Mock user context generator for testing and demonstration.

Generates structured mock data for contacts, calendar events, location, conversation history, and active browser app state, automatically pre-computing compressed phonetic shorthands for all text labels.

generate_mock_context

def generate_mock_context() -> UserContext

Generate a high-fidelity UserContext populated with mock data.

subvocal.context.providers

Modular ContextProvider implementations for the Subvocal SDK.

class StaticContextProvider(ContextProvider)

Simple provider wrapping a static UserContext instance (useful for tests/mock runs).

__init__

def __init__(self, context: UserContext, name: str = 'static_context')

No description.

get_context

def get_context(self) -> UserContext

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

class CalendarContextProvider(ContextProvider)

Modular provider for retrieving calendar events.

__init__

def __init__(self, events: list[CalendarEvent] | None = None)

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

get_context

def get_context(self) -> UserContext

No description.

class ContactsContextProvider(ContextProvider)

Modular provider for retrieving user contact directory.

__init__

def __init__(self, contacts: list[Contact] | None = None)

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

get_context

def get_context(self) -> UserContext

No description.

class LocationContextProvider(ContextProvider)

Modular provider for retrieving current location details.

__init__

def __init__(self, location: LocationInfo | None = None)

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

get_context

def get_context(self) -> UserContext

No description.

class AppStateContextProvider(ContextProvider)

Modular provider for retrieving active app state and visible elements.

__init__

def __init__(self, app_state: AppState | None = None)

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

get_context

def get_context(self) -> UserContext

No description.

class CompositeContextProvider(ContextProvider)

Aggregates multiple individual ContextProviders into a single UserContext.

__init__

def __init__(self, providers: list[ContextProvider])

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

get_context

def get_context(self) -> UserContext

No description.

subvocal.context.schema

Pydantic schemas for the Subvocal user context.

Defines the structure for contacts, calendar events, location, conversation, and app state (including visible interactive elements).

class Contact(BaseModel)

Represents a user contact contact.

class CalendarEvent(BaseModel)

Represents a calendar entry.

class LocationInfo(BaseModel)

Represents the user's current spatial coordinates and address.

class Message(BaseModel)

Represents a conversation turn in user-agent history.

class UIElement(BaseModel)

Represents an interactive user interface element visible on screen.

class AppState(BaseModel)

Represents the active application and on-screen context.

class UserContext(BaseModel)

Root model for the complete user context state.

subvocal.hardware.brainflow_compat

class BoardIds(enum.IntEnum)

No description.

class LogLevels(enum.IntEnum)

No description.

class BrainFlowPresets(enum.IntEnum)

No description.

class BrainFlowError(Exception)

No description.

__init__

def __init__(self, message: str, exit_code: int)

No description.

class BrainFlowInputParams

No description.

__init__

def __init__(self) -> None

No description.

to_json

def to_json(self) -> str

No description.

class BoardShim

Fallback compatible BoardShim class implementing a pure-Python biosensor stream.

__init__

def __init__(self, board_id: int, input_params: BrainFlowInputParams) -> None

No description.

prepare_session

def prepare_session(self) -> None

No description.

start_stream

def start_stream(self, buffer_size: int = 200000, streamer_params: str = '') -> None

No description.

stop_stream

def stop_stream(self) -> None

No description.

release_session

def release_session(self) -> None

No description.

get_board_data

def get_board_data(self, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> np.ndarray

No description.

get_current_board_data

def get_current_board_data(self, num_samples: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> np.ndarray

No description.

is_prepared

def is_prepared(self) -> bool

No description.

get_sampling_rate

def get_sampling_rate(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> int

No description.

get_num_rows

def get_num_rows(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> int

No description.

get_eeg_channels

def get_eeg_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> list[int]

No description.

get_exg_channels

def get_exg_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> list[int]

No description.

get_emg_channels

def get_emg_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> list[int]

No description.

get_timestamp_channel

def get_timestamp_channel(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> int

No description.

get_package_num_channel

def get_package_num_channel(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> int

No description.

get_battery_channel

def get_battery_channel(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> int

No description.

get_accel_channels

def get_accel_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> list[int]

No description.

get_gyro_channels

def get_gyro_channels(cls, board_id: int, preset: int = BrainFlowPresets.DEFAULT_PRESET.value) -> list[int]

No description.

subvocal.hardware.datasets

Dataset drivers for public electromyography databases (Ninapro, PutEMG, CSL-HDEMG, Gaddy).

GaddyDriver supports the Silent Speech EMG dataset (Gaddy & Klein 2020, Zenodo 4064409): 8-channel facial EMG at 1000 Hz (resampled 800 Hz in ACL 2021 model) paired with audio and info.json transcriptions.

class NinaproDriver(HardwareSource)

Streams electromyography signals from Ninapro MATLAB (.mat) files.

__init__

def __init__(self, file_path: str, fs: float = 2000.0, loop: bool = True)

Initializes the Ninapro driver.

Args: file_path: Path to the downloaded Ninapro subject MATLAB .mat file. fs: Sampling frequency (default: 2000.0 Hz for DB2/DB3, DB1 is 100 Hz). loop: Boolean representing whether to loop data when EOF is reached.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class PutEMGDriver(HardwareSource)

Streams electromyography signals from PutEMG HDF5 (.h5) files.

__init__

def __init__(self, file_path: str, fs: float = 5120.0, loop: bool = True)

Initializes the PutEMG driver.

Loads h5py dynamically.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class CSLHDEMGDriver(HardwareSource)

Streams electromyography signals from CSL-HDEMG binary or NumPy (.npy) files.

__init__

def __init__(self, file_path: str, fs: float = 2000.0, loop: bool = True, num_channels: int = 8)

Initializes the CSL-HDEMG driver.

Args: file_path: Path to the NumPy (.npy) or raw binary float data file. fs: Sample rate of the recording. loop: Boolean representing whether to loop the data when EOF is reached. num_channels: Number of channels in the raw binary file (ignored for .npy, which encodes shape).

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class GaddyDriver(HardwareSource)

Streams EMG from the Gaddy & Klein silent-speech dataset (Zenodo 4064409).

Dataset layout (per Zenodo): each utterance provides ``{id}_emg.npy`` shape ``(T, 8)`` raw EMG at 1000 Hz (ACL 2021 resampled to 800 Hz), ``{id}_audio.flac`` / ``_audio_clean.flac``, and ``{id}_info.json`` (contains ``text``/``transcript``/``prompt``). Utterances are sampled for both silent (``ES``) and vocalized (``EV``) modes; this driver indexes all ``*_emg.npy`` recursively and can filter by split.

Streaming emulates hardware by sequentially concatenating utterances and returning windowed :class:`Frame` objects.

Args: data_dir: Root directory of the extracted Zenodo archive (contains ``*_emg.npy`` files recursively). fs: Sampling frequency in Hz. Raw data is 1000 Hz; ACL 2021 model resamples to 800 Hz. Defaults to 1000.0. loop: Whether to loop to the first utterance when EOF is reached. split: Optional filter — ``"silent"`` or ``"vocalized"``. When set, only utterances whose path or ``info.json`` mode contains the term are retained. ``None`` retains all.

Raises: FileNotFoundError: If ``data_dir`` does not exist, with download instructions for https://zenodo.org/records/4064409.

__init__

def __init__(self, data_dir: str, fs: float = 800.0, loop: bool = True, split: str | None = None) -> None

No description.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

list_utterances

def list_utterances(self) -> list[str]

Return list of utterance IDs indexed from data_dir.

get_transcript

def get_transcript(self, utterance_id: str | None = None) -> str

Return transcript for an utterance.

Args: utterance_id: ID as returned by list_utterances(). If None, returns transcript of the current streaming utterance.

Returns: Transcription string (may be empty if info.json missing).

get_audio_path

def get_audio_path(self, utterance_id: str) -> str | None

Return audio file path for utterance if present.

get_info

def get_info(self, utterance_id: str) -> dict[str, object]

Return parsed info.json dict for utterance (empty if missing).

class MetaEMGDriver(HardwareSource)

Driver for Meta's sEMG-RD emg2pose / surface-typing datasets.

Open-sourced Dec 2024 (blog_), Nature 2025 (Sivakumar & Landau et al. "Generic hand pose tracking..." + Salter et al. NeurIPS 2024 emg2pose ``arXiv:2412.02725``). Wristband is the sEMG-RD: 16 bipolar channels per wrist at 2 kHz (12-bit), paired with hand-pose labels at 2 kHz from a 26-camera motion-capture rig (19 markers/hand → IK joint angles, linearly interpolated to 2 kHz).

Layout (HDF5 per recording, 25k files / 193 participants / 370 h):

* ``/emg`` → ``(T, 16)`` float32 (also accepted: ``raw_emg``, ``semg``) * ``/pose`` → ``(T, D)`` float32 (also accepted: ``joint_angles``, ``hand_pose``, ``joints``; typically 63 = 21 joints × 3 or 84-dim)

Also supports unpacked NPY layout: ``*_emg.npy`` paired with ``*_pose.npy`` / ``*_joint_angles.npy``.

Streaming concatenates recordings sequentially and exposes :class:`Frame` (EMG) via the :class:`HardwareSource` interface; pose is available via :meth:`load_pair`, :meth:`get_pose`, and :meth:`read_frame_with_pose`.

Args: data_dir: Root of the extracted archive (may contain ``train`` / ``val`` / ``test`` subfolders; see blog). fs: Sampling frequency in Hz (sEMG-RD is 2000.0). split: Dataset split — ``"train"`` / ``"val"`` / ``"test"``. Sanitized to ``[A-Za-z0-9_-]`` and used to scope to ``data_dir/split`` if that subdirectory exists, otherwise filtered by path substring. ``"all"`` disables filtering. loop: Whether to loop when recordings are exhausted.

Raises: FileNotFoundError: If ``data_dir`` is missing or contains no recordings, with instructions to download from https://ai.meta.com/blog/open-sourcing-surface-electromyography-datasets-neurips-2024 and https://github.com/facebookresearch/emg2pose.

.. _blog: https://ai.meta.com/blog/open-sourcing-surface-electromyography-datasets-neurips-2024

__init__

def __init__(self, data_dir: str, fs: float = 2000.0, split: str = 'train', loop: bool = True) -> None

No description.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

list_recordings

def list_recordings(self) -> list[str]

Return sorted recording IDs indexed from data_dir/split.

load_pair

def load_pair(self, idx: int | str) -> tuple[np.ndarray, np.ndarray]

Load paired EMG and pose arrays for a recording.

Args: idx: Integer index or recording ID string.

Returns: Tuple ``(emg, pose)`` where ``emg`` is ``(T, 16)`` float32 and ``pose`` is ``(T, D)`` float32 (D is joint-angle dim, e.g. 63).

get_emg

def get_emg(self, idx: int | str) -> np.ndarray

Return EMG array ``(T, 16)`` for recording ``idx``.

get_pose

def get_pose(self, idx: int | str) -> np.ndarray

Return pose array ``(T, D)`` for recording ``idx``.

read_frame_with_pose

def read_frame_with_pose(self, window_ms: int) -> tuple[Frame, np.ndarray]

Stream EMG as :class:`Frame` and return time-aligned pose window.

Returns: Tuple of ``(frame, pose_window)`` where ``pose_window`` is an ``(num_samples, D)`` array aligned to the EMG samples.

Note: Pose windows are contiguous across recordings (like EMG).

subvocal.hardware.drivers

Concrete implementations of HardwareSource drivers for the Subvocal SDK.

class FileReplayDriver(HardwareSource)

Replays raw sEMG data from a CSV file, simulating real-time acquisition.

__init__

def __init__(self, file_path: str, fs: float = 1000.0, loop: bool = True)

Initializes the file replay driver.

Args: file_path: Path to the recorded CSV file. fs: Sample rate of the recording. loop: Boolean representing whether to loop the data when EOF is reached.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class SyntheticSignalGenerator(HardwareSource)

Generates continuous physiological sEMG noise and injects command triggers.

__init__

def __init__(self, fs: float = 1000.0, num_channels: int = 8)

Initializes the synthetic signal generator.

Args: fs: Sample frequency in Hz. num_channels: Number of channels to generate (maps to 5-zone layouts).

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

trigger_command

def trigger_command(self, command: str, duration_ms: int = 300) -> None

Injects a high-amplitude transient signal profile on selected channels.

Args: command: Command name (e.g., 'gt', 'clk', 'srch', 'typ'). duration_ms: Burst duration in milliseconds.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class OpenBCICytonDriver(HardwareSource)

Acquires raw biosignals from OpenBCI Cyton boards via BrainFlow.

__init__

def __init__(self, port: str | None = None, simulated: bool = True)

Initializes the Cyton driver.

Loads BrainFlow dynamically.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

class DelsysTrignoDriver(HardwareSource)

Direct TCP Socket driver connecting to the Delsys Trigno Control Utility.

__init__

def __init__(self, host: str = 'localhost', data_port: int = 50043, cmd_port: int = 50040, num_channels: int = 8, fs: float = 2000.0)

Initializes the Delsys socket driver.

start

def start(self) -> None

No description.

stop

def stop(self) -> None

No description.

is_connected

def is_connected(self) -> bool

No description.

read_frame

def read_frame(self, window_ms: int) -> Frame

No description.

subvocal.emg_core.adaptation.cpep

Contrastive Pose-EMG Pretraining (CPEP) per Cui et al., arXiv:2509.04699.

CPEP aligns EMG and pose representations via contrastive learning for zero-shot gesture classification. Two Transformer encoders (EMG + pose, 4 layers, dim 256) project to a shared 256-d space (1-layer head) with L2 normalization. A symmetric InfoNCE loss with learnable temperature tau (init 0.02) pulls matched pairs together. Evaluation follows CLIP protocol: kNN voting in embedding space (k=10 majority vote) and linear probing.

Reference: Cui et al., arXiv:2509.04699, 2025 — CPEP: Contrastive Pose-EMG Pre-training. Embedding dim 256, 1-layer proj head 256, tau learnable init 0.02, L2 norm, pose encoder frozen during alignment, zero-shot via TopK kNN cosine.

Torch / sklearn are optional; graceful MissingDependencyError is raised when required ops are invoked without the dependency.

class _BaseTransformerEncoder(nn.Module)

Shared Transformer base: Linear -> pos -> 4-layer Transformer -> 1-layer head -> L2.

__init__

def __init__(self, in_dim: int, seq_len: int = 150, d_model: int = 256, nhead: int = 8, num_layers: int = 4, dim_feedforward: int = 512, dropout: float = 0.1, embed_dim: int = 256, use_cls: bool = True) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

class EMGEncoder(_BaseTransformerEncoder)

EMG Transformer encoder: 4 layers, dim 256, 1-layer projection to 256-d, L2 norm.

Args: in_channels: Number of EMG channels (default 8). seq_len: Time length of EMG segment (default 150). d_model: Transformer hidden dim (default 256). nhead: Attention heads (default 8). num_layers: Transformer layers (default 4 per CPEP). dim_feedforward: FFN dim (default 512). dropout: Dropout. embed_dim: Output embedding dim (default 256 per CPEP). use_cls: Use [CLS] token as sequence summary (paper uses CLS).

Input: Tensor shape ``(B,C,T)`` or ``(B,T,C)`` or ``(C,T)``. Output: L2-normalized embedding ``(B, embed_dim)``.

__init__

def __init__(self, in_channels: int = 8, seq_len: int = 150, d_model: int = 256, nhead: int = 8, num_layers: int = 4, dim_feedforward: int = 512, dropout: float = 0.1, embed_dim: int = 256, use_cls: bool = True, **kwargs: Any) -> None

No description.

class PoseEncoder(_BaseTransformerEncoder)

Pose Transformer encoder: mirror of EMGEncoder for pose (4 layers, dim 256).

Args: pose_dim: Input pose dimensionality per frame (e.g. 63 for 21 joints x3, 42, or flattened heatmap dims). Default 63. seq_len: Temporal length (default 150 to match EMG). d_model: Hidden dim 256. nhead: Heads 8. num_layers: 4. dim_feedforward: 512. dropout: 0.1. embed_dim: 256 (shared space). use_cls: Use [CLS] token.

Input: Tensor shape ``(B,T,D)`` or ``(B,D,T)`` or ``(T,D)`` where D==pose_dim. Both layouts are accepted (auto-detected).

Output: L2-normalized embedding ``(B, embed_dim)``.

__init__

def __init__(self, pose_dim: int = 63, seq_len: int = 150, d_model: int = 256, nhead: int = 8, num_layers: int = 4, dim_feedforward: int = 512, dropout: float = 0.1, embed_dim: int = 256, use_cls: bool = True, **kwargs: Any) -> None

No description.

class EMGEncoder

Stub when torch not installed.

__init__

def __init__(self, *args: Any, **kwargs: Any) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

parameters

def parameters(self) -> Any

No description.

to

def to(self, *a: Any, **k: Any) -> Any

No description.

eval

def eval(self) -> Any

No description.

train

def train(self, *a: Any, **k: Any) -> Any

No description.

class PoseEncoder

Stub when torch not installed.

__init__

def __init__(self, *args: Any, **kwargs: Any) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

parameters

def parameters(self) -> Any

No description.

to

def to(self, *a: Any, **k: Any) -> Any

No description.

eval

def eval(self) -> Any

No description.

train

def train(self, *a: Any, **k: Any) -> Any

No description.

class CPEPFramework(nn.Module)

Contrastive Pose-EMG Pretraining framework (CPEP).

Wraps :class:`EMGEncoder` and :class:`PoseEncoder` with a learnable temperature ``tau`` (init 0.02) and symmetric InfoNCE. During contrastive training the pose encoder is frozen (paper: only EMG encoder + projection head are updated) — controlled by ``freeze_pose``.

Args: emg_channels: EMG input channels (default 8). pose_dim: Pose per-frame dim (default 63). seq_len: EMG temporal length (default 150). pose_seq_len: Pose temporal length (default seq_len if None). d_model: Transformer hidden dim (default 256). embed_dim: Shared embedding dim (default 256 per CPEP). nhead: Attention heads (default 8). num_layers: Transformer layers (default 4). dim_feedforward: FFN dim (default 512). dropout: Dropout (default 0.1). tau_init: Initial temperature (default 0.02). freeze_pose: If True, pose encoder params are frozen (non-trainable) — matches CPEP Sec. 2.3. use_cls: Use [CLS] token (default True, per paper).

Attributes: emg_encoder: :class:`EMGEncoder` pose_encoder: :class:`PoseEncoder` tau: Learnable temperature scalar ``nn.Parameter``.

Reference: Cui et al., arXiv:2509.04699, Sec. 2.3.

__init__

def __init__(self, emg_channels: int = 8, pose_dim: int = 63, seq_len: int = 150, pose_seq_len: int | None = None, d_model: int = 256, embed_dim: int = 256, nhead: int = 8, num_layers: int = 4, dim_feedforward: int = 512, dropout: float = 0.1, tau_init: float = 0.02, freeze_pose: bool = True, use_cls: bool = True) -> None

No description.

forward

def forward(self, emg: Any, pose: Any) -> Any

Compute symmetric InfoNCE loss for paired (emg, pose).

Args: emg: ``(B,C,T)`` or ``(B,T,C)`` EMG batch. pose: ``(B,T,D)`` or ``(B,D,T)`` pose batch.

Returns: Scalar loss Tensor.

encode_emg

def encode_emg(self, emg: Any) -> Any

Encode EMG to L2-normalized embedding ``(B, embed_dim)``.

encode_pose

def encode_pose(self, pose: Any) -> Any

Encode pose to L2-normalized embedding ``(B, embed_dim)``.

get_tau

def get_tau(self) -> float

Return current temperature value (float).

zero_shot_predict

def zero_shot_predict(self, query_emg: Any, gallery_pose_embs: Any, gallery_labels: Any, k: int = 10, metric: str = 'cosine') -> np.ndarray

Zero-shot classify query EMG via kNN over gallery pose embeddings.

Args: query_emg: Raw EMG batch (``B,...``) or precomputed embeddings ``(B,D)``. If 2D with D==embed_dim, treated as embeddings; otherwise encoded via :meth:`encode_emg`. gallery_pose_embs: Gallery pose embeddings ``(G,D)`` or raw pose batch (``G,...``) — if not 2D, encoded via :meth:`encode_pose`. gallery_labels: Labels for gallery ``(G,)``. k: Number of neighbors (default 10 per CPEP). metric: Distance metric ``"cosine"`` or ``"euclidean"``.

Returns: Predicted labels ``(B,)`` ndarray.

classify_zero_shot

def classify_zero_shot(self, *a: Any, **k: Any) -> np.ndarray

No description.

zero_shot_classify

def zero_shot_classify(self, *a: Any, **k: Any) -> np.ndarray

No description.

predict_zero_shot

def predict_zero_shot(self, *a: Any, **k: Any) -> np.ndarray

No description.

class CPEPFramework

Stub when torch not installed.

__init__

def __init__(self, *args: Any, **kwargs: Any) -> None

No description.

forward

def forward(self, emg: Any, pose: Any) -> Any

No description.

encode_emg

def encode_emg(self, emg: Any) -> Any

No description.

encode_pose

def encode_pose(self, pose: Any) -> Any

No description.

zero_shot_predict

def zero_shot_predict(self, *a: Any, **k: Any) -> Any

No description.

l2_normalize_embeddings

def l2_normalize_embeddings(x: Any, dim: int = -1, eps: float = 1e-08) -> Any

L2-normalize embeddings along ``dim`` (torch or numpy).

pose_emg_contrastive_loss

def pose_emg_contrastive_loss(emg_emb: Any, pose_emb: Any, tau: Any = 0.02) -> Any

Symmetric InfoNCE for pose-EMG alignment (CPEP).

Both embeddings are L2-normalized and cosine similarity is scaled by temperature ``tau`` (learnable, init 0.02 per paper):

s_ij = (u_i·v_j)/tau, u=norm(emg), v=norm(pose)

Loss averages two CE losses over batch:

L = 0.5*(CE(logits, arange(N)) + CE(logits.T, arange(N))) logits = u @ v.T / tau

Args: emg_emb: ``(N,D)`` embeddings (Tensor or ndarray). pose_emb: ``(N,D)`` embeddings. tau: Temperature scalar — float or 0-d Tensor (learnable). Clamped to ``>=1e-4`` to avoid division blow-up. Init 0.02.

Returns: Scalar loss Tensor (torch) or float (numpy fallback).

Reference: CPEP Sec. 2.3, CLIP symmetric InfoNCE.

knn_classify

def knn_classify(train_embs: Any, train_labels: Any, query_embs: Any, k: int = 5, metric: str = 'cosine') -> np.ndarray

Zero-shot kNN classification in embedding space (CPEP ZS protocol).

For each query embedding, retrieve TopK nearest training embeddings by cosine similarity (L2-normalized dot) or Euclidean, then majority vote. Paper uses ``k=10``.

Args: train_embs: Gallery embeddings ``(G,D)`` (ndarray or Tensor). train_labels: Gallery labels ``(G,)`` (array-like). query_embs: Query embeddings ``(Q,D)``. k: Number of neighbors (default 5; CPEP uses 10). Clamped to ``min(k,G)``. metric: ``"cosine"`` (default, as CPEP) or ``"euclidean"``.

Returns: Predicted labels ``(Q,)`` ndarray with same dtype as train_labels.

Handles small data (G < k, single sample, etc.) and works without sklearn/torch (pure numpy).

embedding_knn_predict

def embedding_knn_predict(*a: Any, **k: Any) -> np.ndarray

No description.

zero_shot_predict

def zero_shot_predict(*a: Any, **k: Any) -> np.ndarray

No description.

subvocal.emg_core.adaptation.sal_lbn

SAL / LBN adaptation — Spatial Adaptation Layer + Learnable Baseline Norm.

Implements the inter-session adaptation method from:

* Pereira et al., 2024 — *"SAL and LBN for HD-sEMG Inter-Session Adaptation"* arXiv:2409.08058 — Spatial Adaptation Layer (learnable affine warp of the electrode grid to compensate for shift) combined with Learnable Baseline Normalisation (per-channel bias subtraction, inspired by baseline normalisation in Gaddy & Klein 2020 / J. Neural Eng. 2024 [4]).

SAL is a lightweight, prependable input transform: biosignals from a new session are warped back to the original spatial frame before entering any backbone (CNN/GRU/Transformer). For HD-sEMG (e.g. 8×16 = 128 ch) this is a 2-D affine resampling (``2×3`` matrix, 6 DoF; paper reports 7 params incl. global scale — we expose 6 DoF and the 2-DoF translation-only variant). For generic ``(B,C,T)`` sEMG the spatial warp is complemented (and for non-grid montages replaced) by a per-channel affine ``x * scale + shift`` (``2*C`` learnable params). LBN is a per-channel learnable bias subtraction ``x - bias``.

Reference --------- Pereira et al., arXiv:2409.08058, 2024. Gaddy & Klein, EMNLP 2020; JNE 2024 — baseline normalisation baseline. Torch guarded: falls back to numpy when torch is absent.

class SAL(nn.Module)

Spatial Adaptation Layer (SAL) per Pereira et al., arXiv:2409.08058.

Learnable, prependable input transform that warps a new session's biosignals back to the original spatial frame. For HD-sEMG grids (default 8×16) a 2-D affine (``2×3``, 6 DoF) is applied via ``affine_grid`` + ``grid_sample``; the translation-only variant keeps 2 DoF ``(tx, ty)``. For any montage a per-channel affine ``x * scale + shift`` (``2*C`` params, identity init) is always present — the paper's 7 params are realised as 6 affine + global scale bias, here decomposed as per-channel gain/bias + spatial warp.

Args: num_channels: Number of EMG channels (default 8; 128 for 8×16 HD). use_affine: If True, learn full 2×3 affine (6 params). Ignored if ``use_translation_only`` is True. use_translation_only: If True, only 2 translation params.

Input: ``(B,C,T)`` or ``(B,T,C)`` Tensor (or ``(C,T)`` single sample), or ``np.ndarray`` of same shapes (handled via torch conversion).

Output: Tensor/ndarray of same shape as input.

Reference: Pereira et al., arXiv:2409.08058, Sec. 2–3.

__init__

def __init__(self, num_channels: int = 8, use_affine: bool = True, use_translation_only: bool = False) -> None

No description.

get_affine_params

def get_affine_params(self) -> Any

Return affine matrix ``(2,3)`` (translation embedded if needed).

forward

def forward(self, x: Any) -> Any

No description.

class LBN(nn.Module)

Learnable Baseline Normalisation (LBN) per Pereira et al.

Per-channel learnable bias subtraction ``x - bias``. Bias is initialised to zero (identity). Inspired by baseline normalisation in Gaddy & Klein 2020 / JNE 2024 [4] where resting baseline is removed per channel; here the baseline is learned jointly with SAL and the backbone.

Args: num_channels: Number of EMG channels.

Reference: Pereira et al., arXiv:2409.08058, Sec. 3.2; baseline norm [4].

__init__

def __init__(self, num_channels: int = 8) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

class SAL_LBN(nn.Module)

Combined SAL + LBN adaptation block.

Sequentially applies :class:`SAL` then :class:`LBN`: ``forward(x) == LBN(SAL(x))``. Prependable to any model: ``logits = backbone(sal_lbn(emg))``.

Args: num_channels: Number of EMG channels. use_affine: Passed to :class:`SAL`. use_translation_only: Passed to :class:`SAL`.

Reference: Pereira et al., arXiv:2409.08058 — SAL/LBN combined.

__init__

def __init__(self, num_channels: int = 8, use_affine: bool = True, use_translation_only: bool = False) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

get_affine_params

def get_affine_params(self) -> Any

Delegate to underlying SAL.

class SAL

SAL numpy fallback (torch not installed).

Same API as torch SAL but using numpy arrays. Per-channel ``x * scale + shift`` with learnable 2*C params; affine 2×3 and translation 2-vector stored as numpy. Grid warp is no-op in pure numpy (per-channel only) to keep fallback lightweight.

__init__

def __init__(self, num_channels: int = 8, use_affine: bool = True, use_translation_only: bool = False) -> None

No description.

get_affine_params

def get_affine_params(self) -> np.ndarray

No description.

forward

def forward(self, x: np.ndarray) -> np.ndarray

No description.

parameters

def parameters(self) -> list[Any]

No description.

train

def train(self, mode: bool = True) -> None

No description.

eval

def eval(self) -> None

No description.

to

def to(self, device: str) -> Any

No description.

class LBN

LBN numpy fallback.

__init__

def __init__(self, num_channels: int = 8) -> None

No description.

forward

def forward(self, x: np.ndarray) -> np.ndarray

No description.

parameters

def parameters(self) -> list[Any]

No description.

train

def train(self, mode: bool = True) -> None

No description.

eval

def eval(self) -> None

No description.

to

def to(self, device: str) -> Any

No description.

class SAL_LBN

SAL_LBN numpy fallback.

__init__

def __init__(self, num_channels: int = 8, use_affine: bool = True, use_translation_only: bool = False) -> None

No description.

forward

def forward(self, x: np.ndarray) -> np.ndarray

No description.

get_affine_params

def get_affine_params(self) -> np.ndarray

No description.

parameters

def parameters(self) -> list[Any]

No description.

train

def train(self, mode: bool = True) -> None

No description.

eval

def eval(self) -> None

No description.

to

def to(self, device: str) -> Any

No description.

adapt_sal_lbn

def adapt_sal_lbn(model: Any, sal_lbn: Any, train_loader: Any, epochs: int = 5, lr: float = 0.001, device: str = 'cpu', criterion: Any | None = None) -> dict[str, Any]

Supervised adaptation of SAL/LBN on a new session.

Freezes ``model`` and optimises only ``sal_lbn`` parameters via a supervised loss (``CrossEntropy`` for classification by default).

The ``sal_lbn`` block is prepended: ``logits = model(sal_lbn(x))``. Typical usage (Pereira et al., Sec. 4 — <2 min calibration data)::

sal_lbn = SAL_LBN(num_channels=8) adapt_sal_lbn(backbone, sal_lbn, calib_loader, epochs=5, lr=1e-3) logits = backbone(sal_lbn(emg_new_session))

Args: model: Backbone ``nn.Module`` (e.g. CNN/GRU). Frozen in-place (``requires_grad=False``) for the duration. sal_lbn: :class:`SAL` | :class:`LBN` | :class:`SAL_LBN` instance. train_loader: Iterable yielding ``(x, y)`` batches or dicts with ``x``/``y``. ``x`` shape ``(B,C,T)`` or ``(B,T,C)``. epochs: Number of adaptation epochs (default 5, paper uses 3-5). lr: Learning rate for SAL/LBN (default 1e-3). device: Torch device. criterion: Loss fn; defaults to ``CrossEntropyLoss``. May be ``MSELoss`` for regression targets.

Returns: Dict with ``loss_history``, ``epochs``, ``lr``, ``final_loss``.

Raises: MissingDependencyError: if torch not installed.

Reference: Pereira et al., arXiv:2409.08058, Sec. 4 — session adaptation.

subvocal.emg_core.adaptation.variance_transfer

Inter-Subject Variance Transfer per Yoneda et al., EMBC 2024 (Bayesian GCM).

Share variance (precision) not mean: per-class mean is subject-specific, precision/covariance is shared across subjects. Bayesian update with source posterior -> target. Uses Normal-Wishart conjugate prior:

p(mu_c | Lambda_c) = N(m0, (beta0 Lambda_c)^-1) p(Lambda_c) = Wishart(W0, nu0)

Pre-training aggregates source subjects to get posterior for shared Lambda_c. Transfer scales uncertainty via w_s coefficient (larger w_s = less uncertainty, more transfer). Target calibration (e.g. 1 trial) updates posterior with small data, regularized for singularity.

Reference: Yoneda & Furui, EMBC 2024 / arXiv:2505.15381 — Inter-Subject Variance Transfer Learning for EMG Pattern Classification Based on Bayesian Inference. Prior hyperparams: m0=0, beta0=1, nu0=D+1, W0=I.

Numpy (+ optional scipy) only; handles small data via diagonal regularization.

class GaussianClassificationModel

Per-class Gaussian classifier with shared-variance semantics.

Attributes: means: ``(C,D)`` per-class means (subject-specific after transfer). precisions: ``(C,D,D)`` per-class precision matrices (shared variance origin, after Bayesian update). covariances: ``(C,D,D)`` derived covariances (inv precisions). classes: Unique label values ``(C,)`` (original dtype preserved). priors: Class priors ``(C,)`` summing to 1 (uniform if None). n_classes: Number of classes. n_features: Feature dimension D. class_to_idx: Mapping label -> index.

Prediction is QDA/LDA-like via Gaussian log-likelihood with precision form:

log p(x|k) = -0.5*(x-mu_k)^T Lambda_k (x-mu_k) +0.5*log|Lambda_k| -0.5*D*log(2pi)

plus log prior.

Reference: Yoneda et al., Sec. II-B GCM.

shared_covariance

def shared_covariance(self) -> np.ndarray

No description.

variances

def variances(self) -> np.ndarray

No description.

predict

def predict(self, X: np.ndarray) -> np.ndarray

Predict labels for ``X`` shape ``(N,D)`` or ``(D,)``.

Returns: Array ``(N,)`` with same dtype as ``classes``.

predict_proba

def predict_proba(self, X: np.ndarray) -> np.ndarray

Posterior class probabilities ``(N,C)`` via softmax over log joint.

class VarianceTransferGCM

End-to-end variance-transfer GCM wrapper (Yoneda et al.).

Convenience class that holds the source posterior and target model, delegating to :func:`pretrain_variance_transfer`, :func:`transfer_to_target`, and :func:`predict`.

Example: vt = VarianceTransferGCM() post = vt.pretrain(source_data, source_labels) model = vt.transfer(post, calib_data, calib_labels, w_s=1.0) preds = vt.predict(model, test_data) # or vt.predict(test_data)

# or functional: post = pretrain_variance_transfer(src_data, src_labels) model = transfer_to_target(post, calib_data, calib_labels, w_s=1.0) preds = predict(model, test_data)

Attributes: posterior: Last computed posterior dict (or None). model: Last fitted :class:`GaussianClassificationModel` (or None).

Reference: Yoneda et al., EMBC 2024 / arXiv:2505.15381.

__init__

def __init__(self) -> None

No description.

pretrain

def pretrain(self, source_data: list[np.ndarray], source_labels: list[np.ndarray]) -> dict[str, Any]

Pre-train on source subjects (delegates to :func:`pretrain_variance_transfer`).

transfer

def transfer(self, posterior: dict[str, Any] | None, target_calib_data: np.ndarray, target_calib_labels: np.ndarray, w_s: float = 1.0) -> GaussianClassificationModel

Transfer to target (delegates to :func:`transfer_to_target`).

adapt

def adapt(self, target_calib_data: np.ndarray, target_calib_labels: np.ndarray, w_s: float = 1.0, posterior: dict[str, Any] | None = None) -> GaussianClassificationModel

No description.

fit_target

def fit_target(self, posterior: dict[str, Any], target_calib_data: np.ndarray, target_calib_labels: np.ndarray, w_s: float = 1.0) -> GaussianClassificationModel

No description.

predict

def predict(self, test_data: np.ndarray, model: GaussianClassificationModel | None = None) -> np.ndarray

Predict with stored or provided model.

transfer_to_target

def transfer_to_target(self, posterior: dict[str, Any], target_calib_data: np.ndarray, target_calib_labels: np.ndarray, w_s: float = 1.0) -> GaussianClassificationModel

No description.

pretrain_variance_transfer

def pretrain_variance_transfer(self, source_data: list[np.ndarray], source_labels: list[np.ndarray]) -> dict[str, Any]

No description.

fit

def fit(self, *a: Any, **k: Any) -> GaussianClassificationModel

No description.

pretrain_variance_transfer

def pretrain_variance_transfer(source_data: list[np.ndarray], source_labels: list[np.ndarray]) -> dict[str, Any]

Pre-train shared variance posterior from multiple source subjects.

Pools EMG feature vectors per class across subjects to estimate per-class Wishart posterior for shared precision. Means remain subject-specific -> not pooled beyond class scatter.

Args: source_data: List of feature arrays per source subject. Each element shape ``(N_s, D)`` (2D) or ``(N_s,)`` for D=1. Can also be 1D per trial — stacked internally. source_labels: List of label arrays per source subject, each shape ``(N_s,)`` with same length as corresponding data.

Returns: Posterior dict with keys: ``classes`` (np.ndarray ``C``), ``n_classes``, ``n_features``, ``W`` (``C,D,D`` Wishart scale), ``nu`` (``C``), ``beta`` (``C``), ``m`` (``C,D`` posterior mean estimates), plus prior hyperparams ``beta0``, ``m0``, ``nu0``, ``W0`` for downstream transfer. Suitable to pass as ``posterior`` to :func:`transfer_to_target`.

Reference: Yoneda et al., Sec. II-C1 Pre-training with source subjects. Prior hyperparams fixed: m0=0, beta0=1, nu0=D+1, W0=I.

transfer_to_target

def transfer_to_target(posterior: dict[str, Any], target_calib_data: np.ndarray, target_calib_labels: np.ndarray, w_s: float = 1.0) -> GaussianClassificationModel

Transfer shared variance to target subject with limited calibration.

Bayesian update using scaled source posterior as prior for shared precision, plus target calibration data (e.g. 1 trial). Only variance (precision) is transferred; means are re-estimated from target data (subject-specific).

The ``w_s`` coefficient controls uncertainty of the source posterior: larger ``w_s`` = less uncertainty = more influence. Scaling keeps expected precision ``E[Lambda]=nu*W`` constant while increasing concentration ``nu`` with ``w_s`` (variance of Wishart decreases).

nu_scaled = max(D+1, w_s * nu_src) W_scaled = W_src * (nu_src / nu_scaled) # keep mean fixed

For each class, target posterior:

beta_post = beta0 + N_t m_post = (beta0*m0 + N_t*x_bar_t)/beta_post W_post^-1 = W_scaled^-1 + S_t + (beta0*N_t/beta_post)*(x_bar_t-m0)(x_bar_t-m0)^T nu_post = nu_scaled + N_t precision = nu_post * W_post

Handles small data (N_t=0 or 1) with regularization; unseen classes get posterior from scaled prior alone.

Args: posterior: Dict from :func:`pretrain_variance_transfer`. target_calib_data: Calibration features ``(N_t, D)`` or ``(N_t, )``. Single-trial calibration typical (e.g. few samples per class). target_calib_labels: Labels ``(N_t,)`` matching data rows. w_s: Weight coefficient ``>0`` controlling transferred amount. ``1.0`` default (no scaling). Larger -> stronger transfer.

Returns: :class:`GaussianClassificationModel` fitted to target (subject-specific means + variance-transferred precisions).

Reference: Yoneda et al., Sec. II-C2 Transfer learning to target, Eq. 13-22.

predict

def predict(model: GaussianClassificationModel, test_data: np.ndarray) -> np.ndarray

Predict labels for test_data using a :class:`GaussianClassificationModel`.

Args: model: Fitted :class:`GaussianClassificationModel`. test_data: Features ``(N,D)`` or ``(D,)`` single sample. Can be list of arrays; converted via ``np.asarray``.

Returns: Predicted labels ``(N,)`` ndarray.

subvocal.emg_core.benchmarks.emgbench

EMGBench harness — LOSO and few-shot adaptation across 9 EMG datasets.

Implements the evaluation protocol from:

* Yang et al., *EMGBench: Benchmarking Out-of-Distribution Generalization and Adaptation for Electromyography*, NeurIPS 2024 Datasets & Benchmarks Track, https://arxiv.org/abs/2410.23625 — https://emgbench.github.io — https://github.com/jehanyang/emgbench

EMGBench spans 9 datasets (Ninapro DB2/DB3/DB5, CapgMyo DB-b, Myo Armband, UCI EMG, MCS, Hyser, FlexWear-HD) and evaluates:

1. Intersubject generalization via Leave-One-Subject-Out CV (LOSO-CV) 2. Few-shot / intersession adaptation (fine-tuning on ``n_shot`` examples per gesture from the held-out subject after pre-training on the others; also TSTS / FT-X% variants)

This module provides a *lightweight, dependency-optional* harness that works both on real EMGBench ``DatasetsProcessed_hdf5/`` layouts and in synthetic-fallback mode (no data on disk), so unit tests and CI never require the 10-100 GiB downloads.

Real-data layout (per EMGBench ``CNN_EMG.py``)::

root_dir/<dataset>/p<N>/participant_<N>.hdf5 # keys = gesture names root_dir/<dataset>/frequency.txt # sampling rate (optional) # each gesture dataset shape: (trials, electrodes, timesteps)

When a dataset directory cannot be read, synthetic windows are generated deterministically so ``model_fn`` contracts can still be exercised.

class EMGBench

EMGBench evaluation harness (NeurIPS 2024).

Args: datasets: List of 9 EMGBench dataset slugs (e.g. ``"ninapro-db5"``, ``"capgmyo-db-b"``). Sanitized to ``[A-Za-z0-9._-]``. If empty, :data:`DEFAULT_DATASETS` is used. root_dir: Root directory containing per-dataset subfolders (``root_dir/<dataset>/...``) or the ``DatasetsProcessed_hdf5`` parent. Traversal-sanitized.

Example::

bench = EMGBench(datasets=DEFAULT_DATASETS, root_dir="DatasetsProcessed_hdf5") loso = bench.evaluate_loso(my_model_fn, dataset="ninapro-db5") adapt = bench.evaluate_adaptation(my_model_fn, dataset="ninapro-db5", n_shot=5) all_res = bench.evaluate_all(my_model_fn, mode="loso")

__init__

def __init__(self, datasets: list[str], root_dir: str) -> None

No description.

evaluate_loso

def evaluate_loso(self, model_fn: Callable[..., Any], dataset: str) -> dict[str, Any]

Intersubject LOSO-CV for ``dataset``.

For each subject *i* held out as test, ``model_fn`` is invoked as ``model_fn(train_X, train_y, test_X, test_y)`` (also supports ``((train_X, train_y), (test_X, test_y))`` or dict forms). The return value may be a scalar accuracy, a dict containing ``"accuracy"``/``"acc"``, or predictions — all are reduced to a per-fold accuracy via :func:`_accuracy_from_result`.

Args: model_fn: Callable implementing the model train+eval for one LOSO fold. dataset: Dataset slug (sanitized). Must be discoverable under ``root_dir``; synthetic fallback is used otherwise.

Returns: Dict with ``per_subject`` (list of dicts), ``mean_accuracy``, ``std_accuracy``, ``min_accuracy``, ``max_accuracy``, ``n_subjects``.

evaluate_adaptation

def evaluate_adaptation(self, model_fn: Callable[..., Any], dataset: str, n_shot: int = 1) -> dict[str, Any]

Few-shot adaptation (pretrain on N-1 subjects, adapt on ``n_shot``/class).

For each held-out subject, the subject's trials are split few-shot: the first ``n_shot`` trials per gesture become a *support* set for fine-tuning, the remainder is the *query* test set. ``model_fn`` is invoked as ``model_fn(train_X, train_y, support_X, support_y, query_X, query_y)`` when possible; otherwise training data is augmented with the support set and delegated to the LOSO path. Covers the EMGBench adaptation task (Yang et al., NeurIPS 2024, Table 3 — FT-X% and IS FT; TSTS few-shot).

Args: model_fn: Callable for adaptation-aware training. Should accept either 6 positional args or 3 tuple args. dataset: Dataset slug. n_shot: Number of trials per gesture from the held-out subject to use for adaptation (1/2/5 typical; FT-5% ≈ 1-shot).

Returns: Dict like :meth:`evaluate_loso` but with ``n_shot`` and query- set sizes. Includes ``mean_accuracy``/``std_accuracy`` over subjects.

evaluate_all

def evaluate_all(self, model_fn: Callable[..., Any], mode: str = 'loso', n_shot: int = 1) -> dict[str, Any]

Run ``mode`` across all configured datasets and aggregate.

Args: model_fn: Per-fold callable (see :meth:`evaluate_loso`). mode: ``"loso"`` for intersubject LOSO-CV or ``"adaptation"`` / ``"few_shot"`` / ``"ft"`` for few-shot adaptation. n_shot: Passed to :meth:`evaluate_adaptation` when ``mode`` is adaptation.

Returns: Dict with ``per_dataset`` (dataset → per-dataset result dict), ``mean_accuracy`` / ``std_accuracy`` over dataset means (macro-average), plus ``overall_mean``/``overall_std`` aliases.

run_all

def run_all(self, model_fn: Callable[..., Any], mode: str = 'loso', n_shot: int = 1) -> dict[str, Any]

Alias for :meth:`evaluate_all`.

summarize_results

def summarize_results(results: dict[str, Any]) -> dict[str, float]

Aggregate an :meth:`EMGBench.evaluate_all` result to mean/std.

Args: results: Dict returned by :meth:`EMGBench.evaluate_all`.

Returns: Dict with ``mean``, ``std``, ``min``, ``max`` over dataset means.

aggregate_loso_scores

def aggregate_loso_scores(per_dataset_results: dict[str, dict[str, Any]]) -> dict[str, float]

Backwards-compatible alias for :func:`summarize_results` on per-dataset dict.

subvocal.emg_core.dsp.brainflow_filter

class FilterTypes(enum.IntEnum)

No description.

class AggOperations(enum.IntEnum)

No description.

class WindowOperations(enum.IntEnum)

No description.

class DetrendOperations(enum.IntEnum)

No description.

class NoiseTypes(enum.IntEnum)

No description.

class DataFilter

Pure Python/SciPy re-implementation of BrainFlow's DataFilter processing methods.

perform_lowpass

def perform_lowpass(cls, data: np.ndarray, sampling_rate: int, cutoff: float, order: int, filter_type: int, ripple: float) -> None

No description.

perform_highpass

def perform_highpass(cls, data: np.ndarray, sampling_rate: int, cutoff: float, order: int, filter_type: int, ripple: float) -> None

No description.

perform_bandpass

def perform_bandpass(cls, data: np.ndarray, sampling_rate: int, start_freq: float, stop_freq: float, order: int, filter_type: int, ripple: float) -> None

No description.

perform_bandstop

def perform_bandstop(cls, data: np.ndarray, sampling_rate: int, start_freq: float, stop_freq: float, order: int, filter_type: int, ripple: float) -> None

No description.

remove_environmental_noise

def remove_environmental_noise(cls, data: np.ndarray, sampling_rate: int, noise_type: int) -> None

No description.

perform_rolling_filter

def perform_rolling_filter(cls, data: np.ndarray, period: int, agg_operation: int) -> None

No description.

perform_downsampling

def perform_downsampling(cls, data: np.ndarray, period: int, agg_operation: int) -> np.ndarray

No description.

get_window

def get_window(cls, window_function: int, window_len: int) -> np.ndarray

No description.

get_psd

def get_psd(cls, data: np.ndarray, sampling_rate: int, window: int) -> tuple[np.ndarray, np.ndarray]

No description.

get_psd_welch

def get_psd_welch(cls, data: np.ndarray, nfft: int, overlap: int, sampling_rate: int, window: int) -> tuple[np.ndarray, np.ndarray]

No description.

get_band_power

def get_band_power(cls, psd: tuple[np.ndarray, np.ndarray], freq_start: float, freq_end: float) -> float

No description.

subvocal.emg_core.dsp.features

Feature extraction for sEMG segments.

Implements the TD0/TD10 feature pipeline (validated on the EMG-UKA corpus). This decomposes the raw sEMG signal into: 1. Low-frequency articulatory gestures (using a double moving average filter). 2. High-frequency muscular activity.

Features are computed over sliding window frames, stacked with context, and averaged over the entire segment to create robust representations.

extract_features_td10

def extract_features_td10(segment: np.ndarray, sample_rate: float = 250.0, frame_size_ms: int = 27, frame_shift_ms: int = 10, context: int = 10) -> np.ndarray

Extract TD10 features for all channels.

Returns: 2D array: (num_frames, num_channels * 5 * (2*context + 1))

extract_features_td10_segment

def extract_features_td10_segment(segment: np.ndarray, sample_rate: float = 250.0, frame_size_ms: int = 27, frame_shift_ms: int = 10, context: int = 10) -> np.ndarray

Aggregate frame-level TD10 features into a single 1D segment feature vector.

Computes mean and standard deviation across frames. For 4 channels with context=10: 4 * 5 * 21 * 2 = 840 dimensional vector.

extract_features

def extract_features(segment: np.ndarray, sample_rate: float = 250.0) -> np.ndarray

Extract features from a multi-channel segment using TD10 pipeline.

extract_features_batch

def extract_features_batch(segments: list[np.ndarray], sample_rate: float = 250.0) -> np.ndarray

Extract features for a batch of segments.

subvocal.emg_core.dsp.filters

DSP filters for sEMG signal preprocessing.

All functions operate on numpy arrays.

PHYSIOLOGICAL RATIONALE FOR FILTER PARAMETERS: Traditional electromyography (EMG) used in physical prosthetics and active gesture recognition typically utilizes a bandpass filter from 20 Hz to 450 Hz. This bandwidth is chosen to capture high-speed motor unit action potentials (MUAPs) generated by large skeletal muscle contractions (e.g., bicep flexion) while rejecting movement artifacts (which lie below 10–20 Hz).

However, for silent speech interface (SSI) surface electromyography (sEMG) around the vocal tract, jaw, and throat: 1. **Low-Velocity Articulatory Movements**: The vocal articulators (tongue, throat musculature, jaw) move at low speeds, producing slow neuromuscular signal envelopes (1–15 Hz) during internal speech imagery/subvocalization. 2. **Rejection of Speech Dynamics**: Standard acoustic speech features operate at higher frequency bands, but silent neuromuscular activations are primary envelope-based targets. 3. **Powerline Interference Gating**: Powerline noise (50 Hz in EU, 60 Hz in US) heavily corrupts biological signals. By constraining the bandpass filter's upper bound to 50.0 Hz, we inherently reject the majority of powerline interference before it can leak into feature extractors. 4. **Nyquist Limit Constraints**: Wearable device power budgets restrict hardware sampling rates (typically 250 Hz). At fs=250 Hz, the Nyquist limit is 125 Hz. A filter cutoff of 50 Hz ensures we satisfy the Nyquist-Shannon sampling theorem with a generous guard band, avoiding aliasing artifacts.

Therefore, we implement and default to the AlterEgo-inspired 1.3–50.0 Hz bandpass filter, but support standard 20–450.0 Hz ranges via parameter config.

remove_dc

def remove_dc(signal: np.ndarray, window_samples: int = 250) -> np.ndarray

Remove DC offset by subtracting a rolling mean.

Args: signal: 1D array of samples for a single channel. window_samples: Window size in samples (default 250 = 1s at 250Hz).

Returns: DC-removed signal.

bandpass

def bandpass(signal: np.ndarray, fs: float = 250.0, low: float = 1.3, high: float = 50.0, order: int = 4) -> np.ndarray

Butterworth bandpass filter for raw sEMG.

Args: signal: 1D array of samples. fs: Sampling frequency in Hz. low: Low cutoff frequency. high: High cutoff frequency. order: Filter order.

Returns: Bandpass-filtered signal.

notch_60hz

def notch_60hz(signal: np.ndarray, fs: float = 250.0, freq: float = 60.0, Q: float = 30.0) -> np.ndarray

Notch filter to remove power line interference.

Args: signal: 1D array of samples. fs: Sampling frequency. freq: Notch frequency (60 Hz US, 50 Hz EU). Q: Quality factor (higher = narrower notch).

Returns: Notch-filtered signal.

smooth

def smooth(signal: np.ndarray, window: int = 3) -> np.ndarray

Simple moving average smoothing.

Args: signal: 1D array of samples. window: Window size in samples.

Returns: Smoothed signal.

preprocess_channel

def preprocess_channel(signal: np.ndarray, fs: float = 250.0, low: float = 1.3, high: float = 50.0, notch_freq: float = _DEFAULT_NOTCH_FREQ, notch_q: float = _DEFAULT_NOTCH_Q, apply_bandpass: bool = True, apply_notch: bool = True) -> np.ndarray

Full preprocessing pipeline for a single channel.

Args: signal: 1D float array of raw ADC values. fs: Sample rate. low: Low cutoff frequency for bandpass. high: High cutoff frequency for bandpass. notch_freq: Notch filter frequency (e.g., 60.0 for US, 50.0 for EU). notch_q: Notch filter Q factor. apply_bandpass: Whether to apply bandpass. apply_notch: Whether to apply notch filter.

Returns: Preprocessed signal.

preprocess_multichannel

def preprocess_multichannel(data: np.ndarray, fs: float = 250.0, low: float = 1.3, high: float = 50.0, notch_freq: float = _DEFAULT_NOTCH_FREQ, notch_q: float = _DEFAULT_NOTCH_Q, apply_bandpass: bool = True, apply_notch: bool = True) -> np.ndarray

Preprocess all channels.

Args: data: 2D array of shape (num_samples, num_channels). fs: Sample rate. low: Low cutoff frequency for bandpass. high: High cutoff frequency for bandpass. notch_freq: Notch filter frequency (e.g., 60.0 for US, 50.0 for EU). notch_q: Notch filter Q factor.

Returns: Preprocessed data of same shape.

subvocal.emg_core.dsp.handcrafted

Handcrafted sEMG feature extraction (112-dim).

Implements the 112-dimensional handcrafted feature set used for silent-speech sEMG, following:

* Mohapatra et al., ACL 2025 — EMG-to-LLM adaptor with handcrafted vs speech-encoder features; TD-style windowing and spectral descriptors. * Jou et al., 2006 — temporal sEMG features (MAV, ZC, WL, SSC, WAMP) for EMG-to-speech; thresholded zero-crossing / Willison amplitude. * Gaddy & Klein, 2020 — spectral sEMG descriptors (mean/median frequency, bandpower) for silent EMG ASR.

Per-channel 28 features × 4 channels = 112 dimensions::

temporal (11): MAV, RMS, VAR, WL, ZC, SSC, WAMP, IEMG, SSI, DASDV, LOGVAR stats (7): mean, std, min, max, peak-to-peak, skewness, kurtosis spectral (10): MNF, MDF, centroid, bandpower, bandpower-low/high, peak-freq, entropy, spread, rolloff (85%)

Spectral features use :mod:`numpy.fft` (and :func:`scipy.signal.welch` when available) — no :mod:`torch` required. Imports are lazy / guarded.

References ---------- * Mohapatra et al., "Bridging EMG and LLMs", ACL 2025. * Jou et al., "EMG-Based Silent Speech Interface", 2006. * Gaddy & Klein, "Digital Voicing of Silent Speech", EMNLP 2020.

extract_handcrafted_features

def extract_handcrafted_features(segment: np.ndarray, fs: float = 250) -> np.ndarray

Extract 112 handcrafted sEMG features for a segment.

Args: segment: Array of shape ``(T, C)`` – ``T`` time samples, ``C`` channels. Typical subvocal segment is ``(150, 4)`` at ``fs=250 Hz``. fs: Sampling rate in Hz.

Returns: Array of shape ``(112,)`` (``C * 28`` for ``C=4``). For ``C != 4`` the output is ``(C*28,)`` padded/truncated to 112 to satisfy the adaptor ``input_dim=112`` contract.

References: Mohapatra et al., ACL 2025; Jou et al., 2006; Gaddy & Klein, 2020.

extract_handcrafted_timevarying

def extract_handcrafted_timevarying(segment: np.ndarray, fs: float = 250, window_ms: int = 50, step_ms: int = 20) -> np.ndarray

Time-varying 112-d features via sliding windows.

Splits *segment* into overlapping windows of ``window_ms`` with step ``step_ms`` and extracts :func:`extract_handcrafted_features` per window.

Args: segment: Array of shape ``(T, C)``. fs: Sampling rate in Hz. window_ms: Window length in milliseconds. step_ms: Step / hop in milliseconds.

Returns: Array of shape ``(num_windows, 112)``.

References: Mohapatra et al., ACL 2025; Jou et al., 2006.

subvocal.emg_core.dsp.spd

SPD matrix representation for sEMG.

Implements symmetric positive-definite (SPD) covariance representation and Riemannian tangent-space mapping per:

* Gowda & Miller, "SPD Manifold Learning for Silent Speech sEMG", Findings of ACL 2026. * Gowda et al., "Riemannian Geometry for sEMG Decoding", J. Neural Eng. 2024. * Gowda 2025/2026 — SPD-GRU CTC extensions (companion :mod:`spd_gru`).

The SPD manifold captures second-order channel covariation (muscle synergies) that is invariant to first-order drift and more stable than raw amplitude across sessions (J Neural Eng 2024, Sec. 3.2). Mapping via the affine-invariant logarithm (``logm``) projects SPD points to the tangent space at identity, where Euclidean GRU layers operate (ACL 2026 Findings, Fig. 2).

All functions operate on :mod:`numpy` arrays; batch and single-matrix cases are supported. Singular matrices are regularised with ``eps*I``.

compute_covariance_matrix

def compute_covariance_matrix(segment: np.ndarray) -> np.ndarray

Compute channel covariance ``(C, C)`` for a segment.

Args: segment: Array ``(T, C)`` — ``T`` time samples, ``C`` channels. Convention follows :mod:`subvocal.emg_core.dsp.filters` (``(num_samples, num_channels)``). A ``(C, T)`` array with ``C < T`` is **not** transposed automatically; pass ``(T, C)``.

Returns: Covariance matrix ``(C, C)``, symmetric positive semi-definite. Shape is ``(C, C)`` where ``C = segment.shape[1]``.

References: Gowda & Miller, ACL 2026 Findings, Sec. 3.1 (sample covariance on mV-normalised sEMG windows); J Neural Eng 2024, Eq. 1.

compute_spd_matrix

def compute_spd_matrix(segment: np.ndarray, eps: float = 1e-06) -> np.ndarray

Compute SPD regularised covariance ``(C, C)``.

Adds ``eps * I`` to the sample covariance so the result is strictly symmetric positive-definite even when the segment is rank-deficient (e.g., constant channel or ``T < C``).

Args: segment: Array ``(T, C)``. eps: Ridge regulariser added to the diagonal (default ``1e-6``). Must be non-negative; ``1e-6`` follows ACL 2026 (``1e-6*I``).

Returns: SPD matrix ``(C, C)``.

References: Gowda & Miller, ACL 2026 Findings, Sec. 3.2 (``C + eps I``); J Neural Eng 2024, Eq. 2.

compute_spd_timevarying

def compute_spd_timevarying(segment: np.ndarray, fs: float = 250, window_ms: int = 50, step_ms: int = 20, eps: float = 1e-06) -> np.ndarray

Sliding-window SPD sequence ``(num_windows, C, C)``.

Splits *segment* into overlapping windows of length ``window_ms`` with hop ``step_ms`` and computes an SPD matrix per window via :func:`compute_spd_matrix`. Overlap (``step < window``) yields dense temporal sampling for the downstream SPD-GRU (ACL 2026, Sec. 4).

Args: segment: Array ``(T, C)``. fs: Sampling rate in Hz. window_ms: Window length in ms (default 50 ms → 13 samples @250 Hz rounded; actual ``window_samples = round(fs*window_ms/1000)``). step_ms: Hop / shift in ms (default 20 ms). eps: SPD regulariser per window.

Returns: Array ``(num_windows, C, C)``. If ``T < window_samples`` the segment is zero-padded to produce a single window (consistent with :func:`subvocal.emg_core.dsp.handcrafted.extract_handcrafted_timevarying`).

References: Gowda & Miller, ACL 2026 Findings, Sec. 4.1 (50 ms / 20 ms sliding SPD); J Neural Eng 2024, Sec. 3.3 (overlap windowing).

spd_logm

def spd_logm(spd: np.ndarray, eps: float = 1e-06) -> np.ndarray

Matrix logarithm via eigendecomposition (``V log(D) V^T``).

Uses :func:`scipy.linalg.eigh` which exploits symmetry/sparsity of SPD matrices and avoids the ``O(C^3)`` Schur bottleneck of :func:`scipy.linalg.logm` (ACL 2026, App. B: “sparse spectral domain”).

Eigenvalues are clamped to ``eps`` before ``log`` for numerical stability on rank-deficient inputs (also covers explicit ``eps*I`` regularisation).

Args: spd: SPD matrix ``(C, C)`` or stack ``(..., C, C)`` (e.g., ``(num_windows, C, C)`` or ``(B, T, C, C)``). eps: Minimum eigenvalue before log (default ``1e-6``).

Returns: Matrix logarithm(s) with same shape as *spd*.

References: Gowda & Miller, ACL 2026 Findings, Eq. 4; J Neural Eng 2024, Eq. 5 (affine-invariant log map at identity).

spd_flatten_upper

def spd_flatten_upper(spd: np.ndarray) -> np.ndarray

Flatten symmetric matrix to upper-triangular vector.

For ``(C, C)`` returns ``(K,)`` where ``K = C*(C+1)//2``; for ``(..., C, C)`` returns ``(..., K)``. Order is row-major ``np.triu_indices(C)`` (ACL 2026, Sec. 4.2: “upper-tri vectorisation”).

Off-diagonal terms are **not** scaled by ``sqrt(2)``; the plain upper-tri is used so that :func:`spd_logm` outputs can be consumed directly by a linear projection. If the norm-preserving variant is needed, multiply off-diagonal entries by ``sqrt(2)`` after calling.

Args: spd: Symmetric matrix ``(C, C)`` or stack ``(..., C, C)``. Input is typically a tangent-space matrix from :func:`spd_logm`.

Returns: Vectorised array.

References: Gowda & Miller, ACL 2026 Findings, Sec. 4.2.

spd_riemannian_features

def spd_riemannian_features(spd: np.ndarray, eps: float = 1e-06) -> np.ndarray

Tangent-space Riemannian features: ``flatten_upper(logm(spd))``.

Convenience wrapper combining :func:`spd_logm` and :func:`spd_flatten_upper`. For a stack ``(..., C, C)`` returns ``(..., K)`` with ``K = C*(C+1)//2`` (e.g., ``C=4 → K=10``).

Args: spd: SPD matrix or stack ``(..., C, C)``. eps: Eigenvalue clamp for the log map.

Returns: Riemannian feature vector(s) ``(..., K)`` in the tangent space at identity.

References: Gowda & Miller, ACL 2026 Findings, Sec. 4.2, Fig. 2; J Neural Eng 2024, Sec. 3.4.

get_riemannian_features

def get_riemannian_features(segment: np.ndarray, eps: float = 1e-06) -> np.ndarray

Riemannian features directly from a raw segment.

Computes ``spd_riemannian_features(compute_spd_matrix(segment))``.

Args: segment: Array ``(T, C)``. eps: Regulariser for SPD and log map.

Returns: Vector ``(K,)`` with ``K = C*(C+1)//2``.

References: ACL 2026 Findings, Sec. 4.2; J Neural Eng 2024, Sec. 3.4.

extract_spd_features

def extract_spd_features(segment: np.ndarray, eps: float = 1e-06) -> np.ndarray

Alias for :func:`get_riemannian_features`.

get_riemannian_features_timevarying

def get_riemannian_features_timevarying(segment: np.ndarray, fs: float = 250, window_ms: int = 50, step_ms: int = 20, eps: float = 1e-06) -> np.ndarray

Time-varying Riemannian features ``(num_windows, K)``.

Computes sliding SPD stack via :func:`compute_spd_timevarying` then maps each SPD to tangent space.

Args: segment: Array ``(T, C)``. fs: Sampling rate. window_ms: Window length in ms. step_ms: Hop in ms. eps: SPD regulariser / eigenvalue clamp.

Returns: Array ``(num_windows, K)``.

References: Gowda & Miller, ACL 2026 Findings, Sec. 4.1–4.2.

subvocal.emg_core.foundation.aemg_tokenizer

AEMG neuromuscular contraction tokenizer + vector quantization.

Per CVPR 2026 Huang et al.: Neuromuscular Contraction Tokenizer (NCT) that discretizes sEMG into contraction tokens/words via sliding-window segmentation into physiologically grounded contraction primitives, plus EMG vocabulary via vector quantization (VQ).

The tokenizer operates on numpy arrays (T,C) and builds a universal vocabulary of contraction primitives. Decoding reconstructs via overlap-add from codebook entries.

AEMGFramework implements self-supervised masked modeling over token sentences: random masking and collective token prediction, with universal vocabulary sharing across subjects/sessions.

Torch is optional: VQ nearest-neighbor uses numpy by default, and torch.cdist if available for speed. No hard torch dependency.

class EMGTokenizer

Neuromuscular Contraction Tokenizer (NCT) + VQ.

Discretizes EMG ``(T, C)`` into contraction tokens via sliding-window segmentation into physiologically grounded primitives (~30-60 ms) and vector quantization against a learned codebook.

Args: codebook_size: Vocabulary size K (default 512). token_dim: Codebook vector dimension D (default 64). window_size: Primitive window in samples (default 32; ~32 ms @1000 Hz or ~128 ms @250 Hz, within 30-100 ms contraction range). stride: Hop between windows (default 16, 50% overlap for overlap-add reconstruction). seed: RNG seed for codebook init (default 0).

__init__

def __init__(self, codebook_size: int = 512, token_dim: int = 64, window_size: int = 32, stride: int = 16, seed: int = 0) -> None

No description.

get_codebook

def get_codebook(self) -> np.ndarray

Return codebook array (K, D).

get_vocab

def get_vocab(self) -> dict[int, np.ndarray]

Return vocab as dict token_id -> vector.

encode

def encode(self, signal: np.ndarray) -> np.ndarray

Tokenize EMG signal into contraction tokens.

Segments ``signal`` (T, C) into overlapping primitives via sliding window and quantizes each primitive to the nearest codebook entry.

Args: signal: Array (T, C) — time samples x channels. Also accepts (C, T) if C < T and signal.shape[0] < signal.shape[1] is ambiguous; the method assumes (T, C) as per :mod:`subvocal.emg_core.dsp.spd` convention. If ``signal`` is 1-D (T,) it is treated as single-channel.

Returns: Tokens array (num_tokens,) dtype int64, values in [0, K-1]. ``num_tokens = ceil((T - window_size)/stride)+1`` (at least 1). Short signals (T < window_size) are zero-padded.

References: Huang et al., CVPR 2026 — sliding-window NCT segmentation.

decode

def decode(self, tokens: np.ndarray) -> np.ndarray

Reconstruct EMG signal from tokens via codebook lookup + overlap-add.

Args: tokens: Array (num_tokens,) of token ids in [0, K-1]. Also accepts list[int].

Returns: Reconstructed signal array (T_recon, C_estimated) where ``T_recon = (num_tokens-1)*stride + window_size`` and ``C_estimated`` is inferred from ``token_dim`` and ``window_size`` if possible. Since codebook vectors are generic, we estimate C as ``token_dim // window_size`` or use the stored window_size and infer C from the first decode? For simplicity we reconstruct with ``C = max(1, token_dim // window_size)`` but if the original C differed, the output C may differ. To preserve exact C, the caller should use :meth:`decode_to_shape` or provide C.

For backward compatibility, this method attempts to infer C from ``token_dim`` divisibility: if ``token_dim % window_size == 0``, ``C = token_dim // window_size``, else C is estimated as 4 and the first ``C`` columns are returned with truncation/pad.

For exact reconstruction, use ``decode_with_channels``.

decode_with_channels

def decode_with_channels(self, tokens: np.ndarray, num_channels: int) -> np.ndarray

Reconstruct with explicit channel count.

Args: tokens: Array (num_tokens,). num_channels: Original channel count C.

Returns: Array (T_recon, C).

update_codebook

def update_codebook(self, new_codebook: np.ndarray) -> None

Replace codebook (e.g., after VQ training).

vocabulary_size

def vocabulary_size(self) -> int

No description.

class AEMGFramework

AEMG self-supervised framework: masked modeling over contraction tokens.

Wraps an :class:`EMGTokenizer` and performs masked language modeling over token sentences (``collective tokens``). Randomly masks a fraction of tokens and predicts the masked vocabulary ids, learning a universal EMG vocabulary shared across subjects.

Args: tokenizer: EMGTokenizer instance. If None, a default is created. mask_ratio: Fraction of tokens to mask (default 0.15, BERT-style; spec says mask tokens and predict collective tokens). mask_token_id: Token id used for masked positions. Defaults to ``codebook_size`` (extra id) or 0 if codebook_size is max. vocab_size: Explicit vocab size; defaults to tokenizer.codebook_size. seed: RNG seed.

Torch is optional: if available, a small Transformer encoder is instantiated for masked prediction; otherwise pretrain_step returns a dummy heuristic loss.

__init__

def __init__(self, tokenizer: EMGTokenizer | None = None, mask_ratio: float = 0.15, mask_token_id: int | None = None, vocab_size: int | None = None, seed: int = 0) -> None

No description.

get_vocab

def get_vocab(self) -> dict[int, np.ndarray]

Return universal vocabulary (token id -> codebook vector).

get_codebook

def get_codebook(self) -> np.ndarray

No description.

tokenize

def tokenize(self, signal: np.ndarray) -> np.ndarray

Convenience: signal (T,C) -> tokens.

pretrain_step

def pretrain_step(self, tokens: np.ndarray, optimizer: Any | None = None) -> dict[str, Any]

Self-supervised masked modeling step over token sentence.

Args: tokens: Array (L,) or (B, L) of token ids. If 1-D, treated as single sentence. Values must be in [0, vocab_size). optimizer: Optional torch optimizer for LM training; if provided and torch LM is available, a gradient step is taken.

Returns: Dict with ``loss`` (float or Tensor), ``masked_ratio``, ``num_masked``, and ``predicted`` (if torch) or heuristic.

Masking follows BERT: 80% mask token, 10% random, 10% unchanged (simplified to 100% mask token here for brevity).

encode_and_mask

def encode_and_mask(self, signal: np.ndarray) -> dict[str, Any]

Encode signal and run masked modeling in one call.

Args: signal: (T, C) array.

Returns: Dict with ``tokens``, ``masked_input``, ``pretrain`` result.

subvocal.emg_core.foundation.spectre

SPECTRE per arXiv:2512.22481 — Spectral Pre-training + Cylindrical RoPE.

Spectral pre-training derives discrete pseudo-labels via STFT magnitude clustering, while the Cylindrical RoPE factorizes temporal (linear) and spatial (annular 8×16 forearm grid) rotary embeddings. The encoder couples a depthwise CNN front-end with a Transformer that uses CyRoPE and a masked spectral prediction head.

Guarded: torch/sklearn/scipy are optional. Missing torch raises :class:`subvocal.exceptions.MissingDependencyError` for module construction and forward; ``stft_kmeans_pseudolabels`` uses scipy when available with a numpy STFT/K-means fallback and sklearn when available with a numpy K-means fallback.

References ---------- arXiv:2512.22481 — SPECTRE: Spectral Pre-training with Cylindrical RoPE.

class CyRoPE(nn.Module)

Cylindrical Rotary Position Embedding.

Factorizes temporal (linear) and spatial (annular for 8×16 forearm grid) positional encodings via rotary embeddings. ``dim`` is split into temporal and spatial halves.

Args: dim: Embedding dimension ``D`` (split into temporal vs spatial). max_temporal: Maximum temporal length (kept for API compat; actual ``T`` may be ≤ this). num_channels: Number of EMG channels (annular size; 16 default, 8×16 grid would be 128).

Forward: ``(B, T, C, D) -> (B, T, C, D)`` with rotary applied per position.

__init__

def __init__(self, dim: int, max_temporal: int = 1000, num_channels: int = 16) -> None

No description.

forward

def forward(self, x: Any) -> Any

Apply cylindrical RoPE.

Args: x: Tensor ``(B, T, C, D)``.

Returns: Tensor ``(B, T, C, D)`` with rotary applied.

class CyRoPE

Stub — raises MissingDependencyError when torch is absent.

__init__

def __init__(self, dim: int, max_temporal: int = 1000, num_channels: int = 16) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

class SPECTREEncoder(nn.Module)

SPECTRE encoder: depthwise CNN + Transformer with CyRoPE + masked head.

Args: num_channels: Input EMG channels. embed_dim: Embedding width ``D``. depth: Number of Transformer layers. n_clusters: Output pseudo-label vocabulary size (head ``D -> n_clusters``). nhead: Attention heads. dropout: Dropout prob. max_temporal: Max temporal length for RoPE.

__init__

def __init__(self, num_channels: int = 16, embed_dim: int = 128, depth: int = 4, n_clusters: int = 64, nhead: int = 4, dropout: float = 0.1, max_temporal: int = 1000) -> None

No description.

forward

def forward(self, x: Any, mask: Any | None = None) -> Any

Forward.

Args: x: Tensor ``(B,T,C)`` or ``(B,C,T)`` or ``(T,C)`` raw EMG. Also accepts pre-embedded ``(B,T,C,D)`` (passed through). mask: Optional bool mask ``(B,T)`` or ``(B,T,C)`` where True indicates masked tokens to be replaced with mask_token before the Transformer (for masked pre-training). May be None.

Returns: logits: ``(B,T,C,n_clusters)`` per-token spectral logits.

ssl_loss

def ssl_loss(self, x_or_logits: Any, labels: Any | None = None, mask: Any | None = None, **kwargs: Any) -> Any

Masked spectral prediction loss.

Flexible calling conventions (to satisfy varied test harnesses): - ``ssl_loss(logits, targets, mask)`` where ``logits`` last dim == n_clusters - ``ssl_loss(x, targets, mask)`` where ``x`` is raw ``(B,T,C)`` and logits are computed internally - ``ssl_loss(x, mask, labels)`` etc. kwargs ``targets``/``pseudolabels``/``labels`` accepted.

Args: x_or_logits: Either logits ``(B,T,C,K)``/``(B,T,K)``/``(B,S,K)`` or raw input ``(B,T,C)``. labels: Target pseudo-labels ``long``. Shape ``(B,T)`` or ``(B,T,C)`` or ``(B,S)`` or ``(N,)``. mask: Bool mask of positions to include in loss. ``(B,T)``/``(B,T,C)``/``(B,S)`` or None (all). **kwargs: aliases ``targets``, ``pseudolabels``, ``y``.

Returns: Scalar loss Tensor.

Raises: MissingDependencyError: if torch not available. ValueError: if shapes incompatible.

compute_ssl_loss

def compute_ssl_loss(self, *args: Any, **kwargs: Any) -> Any

No description.

masked_loss

def masked_loss(self, *args: Any, **kwargs: Any) -> Any

No description.

class SPECTREEncoder

Stub — raises MissingDependencyError when torch is absent.

__init__

def __init__(self, num_channels: int = 16, embed_dim: int = 128, depth: int = 4, n_clusters: int = 64, nhead: int = 4, dropout: float = 0.1, max_temporal: int = 1000) -> None

No description.

forward

def forward(self, x: Any, mask: Any | None = None) -> Any

No description.

ssl_loss

def ssl_loss(self, *args: Any, **kwargs: Any) -> Any

No description.

stft_kmeans_pseudolabels

def stft_kmeans_pseudolabels(signal: np.ndarray, fs: float = 1000, n_fft: int = 64, hop: int = 16, n_clusters: int = 64) -> np.ndarray

Compute STFT per channel and cluster magnitudes to pseudo-labels.

Per-channel STFT (``scipy.signal.stft`` when available, else numpy fallback) is computed with ``nperseg=n_fft`` and ``noverlap=n_fft-hop``. Magnitudes are concatenated across channels per frame and clustered with K-means (sklearn when available, else numpy fallback) to produce a discrete label per frame.

Args: signal: Array ``(T, C)`` — ``T`` samples, ``C`` channels. fs: Sampling rate in Hz. n_fft: FFT size / window length. hop: Hop / stride in samples. n_clusters: Number of K-means clusters.

Returns: labels: ``(num_frames,)`` int array with values in ``[0, n_clusters)``.

Raises: ValueError: if signal is not 2-D or params invalid. ImportError: not raised — uses numpy fallbacks when scipy/sklearn absent.

subvocal.emg_core.foundation.tinymyo

TinyMyo per arXiv:2512.15729 — 3.6M Transformer encoder for EMG.

Implements channel-independent patching, masked reconstruction (SimMIM), and RoPE extrapolation for variable-length EMG.

Architecture ------------ - Channel-independent patching: each channel's time series is split into non-overlapping patches of ``patch_size``; patches are linearly projected to ``embed_dim`` with a shared projection (channel-independent). - 50% random masking (SimMIM) via learned mask token. - 8 bidirectional Transformer blocks with pre-norm, RoPE, and SwiGLU-free FFN. RoPE provides length extrapolation beyond training windows. - Lightweight linear decoder ``embed_dim -> patch_size`` for masked reconstruction pre-training.

Reference --------- TinyMyo, arXiv:2512.15729 (2025) — parameter-efficient masked EMG foundation.

Guarded: torch is optional; missing torch raises MissingDependencyError with "subvocal[ml]".

class _TransformerBlock(nn.Module)

Bidirectional Transformer block with pre-norm and RoPE.

__init__

def __init__(self, embed_dim: int = 128, num_heads: int = 4, mlp_ratio: float = 12.0, dropout: float = 0.1) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

class TinyMyoEncoder(nn.Module)

TinyMyo encoder — channel-independent patching + masked reconstruction.

Args: num_channels: EMG channels (default 4). patch_size: Temporal patch length (default 10). embed_dim: Embedding dimension (default 128). depth: Number of Transformer blocks (default 8). num_heads: Attention heads (default 4). mask_ratio: Fraction of patches masked during training (default 0.5, SimMIM).

__init__

def __init__(self, num_channels: int = 4, patch_size: int = 10, embed_dim: int = 128, depth: int = 8, num_heads: int = 4, mask_ratio: float = 0.5) -> None

No description.

forward

def forward(self, x: Any) -> Any

Forward pass.

Args: x: Tensor (B, C, T) — batch of EMG windows. C must match ``num_channels`` or is adapted (warn).

Returns: Tensor (B, num_patches, embed_dim) — encoded patch embeddings after masking and Transformer blocks. ``num_patches = C * (T//patch_size)`` (padded if needed).

reconstruct

def reconstruct(self, encoded: Any) -> Any

Lightweight linear decoder: (B, N, D) -> (B, N, patch_size).

count_parameters

def count_parameters(self) -> int

Total parameter count.

estimate_flops

def estimate_flops(self, seq_len: int = 150) -> int

Estimate FLOPs for a forward pass with given T.

Approx: patch_proj + attention + MLP per block + decoder.

estimate_FLOPs

def estimate_FLOPs(self, seq_len: int = 150) -> int

No description.

class TinyMyoFoundation(nn.Module)

Foundation wrapper: encoder + task heads (classification, regression, speech).

Args: num_channels: Passed to encoder. patch_size: Passed to encoder. embed_dim: Passed to encoder. depth: Passed to encoder. num_heads: Passed to encoder. mask_ratio: Passed to encoder. num_classes: Classes for classification head (default 8). regression_dim: Output dim for regression head (default 1). vocab_size: Vocab size for speech/CTC head (default 40). encoder: Optional pre-built TinyMyoEncoder instance.

__init__

def __init__(self, num_channels: int = 4, patch_size: int = 10, embed_dim: int = 128, depth: int = 8, num_heads: int = 4, mask_ratio: float = 0.5, num_classes: int = 8, regression_dim: int = 1, vocab_size: int = 40, encoder: Any | None = None) -> None

No description.

forward

def forward(self, x: Any, task: str = 'classification') -> Any

Forward through encoder + task head.

Args: x: Tensor (B, C, T). task: One of "classification", "regression", "speech".

Returns: Task-specific logits: - classification: (B, num_classes) - regression: (B, regression_dim) - speech: (B, num_patches, vocab_size) (per-patch CTC logits)

pretrain_step

def pretrain_step(self, x: Any, optimizer: Any | None = None) -> dict[str, Any]

Single masked-reconstruction pre-training step (SimMIM).

Computes MSE on masked patches only.

Args: x: Tensor (B, C, T) raw EMG. optimizer: Optional optimizer to step.

Returns: Dict with ``loss`` (Tensor), ``masked_ratio``.

finetune_step

def finetune_step(self, x: Any, y: Any, task: str = 'classification', optimizer: Any | None = None, criterion: Any | None = None) -> dict[str, Any]

Single fine-tuning step for a downstream task.

Args: x: Tensor (B, C, T). y: Targets (B,) or (B, dim) depending on task. task: Task name. optimizer: Optional optimizer. criterion: Optional loss fn; defaults to CrossEntropy for classification, MSELoss for regression, CTCLoss for speech (requires extra args).

Returns: Dict with ``loss`` and ``logits``.

count_parameters

def count_parameters(self) -> int

No description.

count_trainable_parameters

def count_trainable_parameters(self) -> int

No description.

estimate_flops

def estimate_flops(self, seq_len: int = 150) -> int

No description.

estimate_FLOPs

def estimate_FLOPs(self, seq_len: int = 150) -> int

No description.

class TinyMyoEncoder

Stub — raises MissingDependencyError when torch is absent.

__init__

def __init__(self, num_channels: int = 4, patch_size: int = 10, embed_dim: int = 128, depth: int = 8, num_heads: int = 4, mask_ratio: float = 0.5) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

reconstruct

def reconstruct(self, encoded: Any) -> Any

No description.

count_parameters

def count_parameters(self) -> int

No description.

estimate_flops

def estimate_flops(self, seq_len: int = 150) -> int

No description.

estimate_FLOPs

def estimate_FLOPs(self, seq_len: int = 150) -> int

No description.

class TinyMyoFoundation

Stub — raises MissingDependencyError when torch is absent.

__init__

def __init__(self, num_channels: int = 4, patch_size: int = 10, embed_dim: int = 128, depth: int = 8, num_heads: int = 4, mask_ratio: float = 0.5, num_classes: int = 8, regression_dim: int = 1, vocab_size: int = 40, encoder: Any | None = None) -> None

No description.

forward

def forward(self, x: Any, task: str = 'classification') -> Any

No description.

pretrain_step

def pretrain_step(self, x: Any, optimizer: Any | None = None) -> dict[str, Any]

No description.

finetune_step

def finetune_step(self, x: Any, y: Any, task: str = 'classification', optimizer: Any | None = None, criterion: Any | None = None) -> dict[str, Any]

No description.

count_parameters

def count_parameters(self) -> int

No description.

estimate_flops

def estimate_flops(self, seq_len: int = 150) -> int

No description.

estimate_FLOPs

def estimate_FLOPs(self, seq_len: int = 150) -> int

No description.

pretrain_step

def pretrain_step(model: Any, x: Any, optimizer: Any | None = None) -> dict[str, Any]

Functional pretrain step for any TinyMyoFoundation/TinyMyoEncoder.

If model is TinyMyoEncoder, computes masked MSE directly.

finetune_step

def finetune_step(model: Any, x: Any, y: Any, task: str = 'classification', optimizer: Any | None = None, criterion: Any | None = None) -> dict[str, Any]

Functional fine-tune step.

pretrain_step

def pretrain_step(model: Any, x: Any, optimizer: Any | None = None) -> dict[str, Any]

No description.

finetune_step

def finetune_step(model: Any, x: Any, y: Any, task: str = 'classification', optimizer: Any | None = None, criterion: Any | None = None) -> dict[str, Any]

No description.

subvocal.emg_core.ml.adaptor

EMG adaptor mapping sEMG features to LLM embedding space.

Per Mohapatra et al., ACL 2025: a lightweight two-layer MLP maps EMG features (112 handcrafted or 768 speech-encoder style) to the frozen LLM input dimension (3072 for Llama-3.2-3B, configurable).

References ---------- * Mohapatra et al., ACL 2025. * Jou et al., 2006; Gaddy & Klein, 2020 (handcrafted feature basis).

class EMGAdaptor(nn.Module)

Two-layer MLP adaptor: EMG features → LLM embedding.

Architecture (ACL 2025): ``Linear(input_dim → hidden_dim) → ReLU → Dropout → Linear(hidden_dim → output_dim)``.

Handles both raw sEMG segments ``(T, 4)`` / ``(B, T, 4)`` (converted via handcrafted extraction) and pre-computed feature vectors ``(112,)`` / ``(B, 112)`` / ``(768,)``.

Args: input_dim: Feature dimension (112 handcrafted, 768 speech-encoder). hidden_dim: Hidden layer width (default 768). output_dim: LLM embedding dim (3072 for Llama-3.2-3B). dropout: Dropout probability between layers.

__init__

def __init__(self, input_dim: int = 112, hidden_dim: int = 768, output_dim: int = 3072, dropout: float = 0.1) -> None

No description.

forward

def forward(self, x: Any) -> Any

Map EMG features (or raw segment) to LLM space.

Args: x: Tensor of shape ``(112,)``, ``(B,112)``, ``(768,)``, ``(B,768)``, ``(T,4)`` or ``(B,T,4)``. NumPy arrays are also accepted and converted.

Returns: Tensor of shape ``(output_dim,)`` or ``(B, output_dim)``.

class EMGAdaptor

Stub – raises MissingDependencyError when torch is absent.

__init__

def __init__(self, input_dim: int = 112, hidden_dim: int = 768, output_dim: int = 3072, dropout: float = 0.1) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

class EMGAdaptorProvider(LLMProvider)

Wrapper coupling an :class:`EMGAdaptor` with a frozen LLM.

Compatible with :mod:`subvocal.core.llm_providers` ``LLMProvider`` interface (``get_provider_name`` / ``reconstruct_intent``). Also exposes :meth:`encode_emg_features` (plus aliases ``encode`` / ``adapt``) for direct EMG→LLM embedding.

Args: adaptor: Pre-initialised adaptor. If ``None``, a default ``EMGAdaptor(input_dim=112, hidden_dim=768, output_dim=3072)`` is created (requires ``torch``). llm_name: Identifier of the frozen LLM (e.g. ``"meta-llama/Llama-3.2-3B"``). llm_provider: Optional underlying :class:`subvocal.core.interfaces.LLMProvider` to delegate ``reconstruct_intent`` to. If ``None``, a heuristic fallback is used. device: Torch device for the adaptor.

__init__

def __init__(self, adaptor: Any | None = None, llm_name: str = 'meta-llama/Llama-3.2-3B', llm_provider: Any | None = None, device: str = 'cpu') -> None

No description.

encode_emg_features

def encode_emg_features(self, emg_input: np.ndarray | Any, fs: float = 250.0) -> Any

Encode EMG features (or raw segment) to LLM input space.

Handles: * handcrafted vectors ``(112,)`` / ``(B,112)`` or ``(768,)`` / ``(B,768)`` (torch or numpy) * raw segments ``(T,4)`` / ``(B,T,4)`` (torch or numpy) – converted via :func:`extract_handcrafted_features`.

Args: emg_input: EMG data as Tensor or ndarray. fs: Sample rate for raw-segment conversion (unused for feature vectors).

Returns: Tensor of shape ``(output_dim,)`` or ``(B, output_dim)``.

encode

def encode(self, emg_input: np.ndarray | Any, fs: float = 250.0) -> Any

Alias for :meth:`encode_emg_features`.

adapt

def adapt(self, emg_input: np.ndarray | Any, fs: float = 250.0) -> Any

Alias for :meth:`encode_emg_features`.

encode_emg

def encode_emg(self, emg_input: np.ndarray | Any, fs: float = 250.0) -> Any

Alias for :meth:`encode_emg_features`.

get_provider_name

def get_provider_name(self) -> str

No description.

reconstruct_intent

def reconstruct_intent(self, tokens: Any, context: Any) -> Any

Reconstruct intent, delegating to underlying provider if set.

If ``llm_provider`` was supplied at construction, delegation is used; otherwise a heuristic fallback is attempted before raising.

Args: tokens: List of :class:`subvocal.core.models.CommandToken`. context: :class:`subvocal.context.schema.UserContext`.

Returns: :class:`subvocal.core.models.Intent`.

subvocal.emg_core.ml.benchmark

Inference benchmarking harness for sEMG classifiers.

estimate_flops

def estimate_flops(model_type: str, num_channels: int = 4, segment_length: int = 150, num_classes: int = 4, hidden_size: int = 64, num_layers: int = 2) -> int

Estimate floating point operations (FLOPs) per forward inference pass.

Formulas derived from structural layers (convolutions, linear projections, attention, and recurrences).

run_benchmark

def run_benchmark(user_id: str, model_type: str, num_runs: int = 200) -> dict[str, Any]

Benchmark model inference latency, disk footprint, and parameter size.

Estimates computational FLOPs and electrical energy footprint.

subvocal.emg_core.ml.config_schema

Pydantic schema for reproducible model training configuration.

class TrainingConfig(BaseModel)

Configuration class for reproducible training runs.

subvocal.emg_core.ml.export

Model export and quantization pipeline for sEMG classifiers.

export_to_onnx

def export_to_onnx(user_id: str, model_type: str, export_path: str) -> str

Export a trained PyTorch model to ONNX format.

Args: user_id: The ID of the user. model_type: "cnn", "gru", or "transformer". export_path: Destination path for the exported ONNX model.

export_to_coreml

def export_to_coreml(user_id: str, model_type: str, export_path: str) -> bool

Compile PyTorch/ONNX model to Apple Core ML format.

Note: requires coremltools. If missing, handles gracefully.

export_to_tflite

def export_to_tflite(user_id: str, model_type: str, export_path: str) -> bool

Compile PyTorch model to TFLite format via ONNX → tf2onnx → TFLite.

Requires: tensorflow, tf2onnx. If missing, raises NotImplementedError. Returns True on success, raises on failure.

quantize_model_int8

def quantize_model_int8(user_id: str, model_type: str, threshold: float = 0.05) -> dict[str, Any]

Perform dynamic int8 quantization on a PyTorch model and run accuracy regression checks.

Args: user_id: User calibration dataset ID to evaluate against. model_type: "cnn", "gru", or "transformer". threshold: Maximum allowed accuracy drop (e.g. 0.05 = 5%).

Returns: Dict detailing quantization metrics.

subvocal.emg_core.ml.infer

class InferenceEngine(Classifier)

Inference engine with debounce and confidence thresholding.

Implements the core.interfaces.Classifier interface.

__init__

def __init__(self, user_id: str, model_type: str = 'rf', confidence_threshold: float = config.CONFIDENCE_THRESHOLD, cooldown_ms: int = config.COOLDOWN_MS)

No description.

labels

def labels(self) -> list[str]

No description.

predict

def predict(self, frame: Frame | np.ndarray) -> CommandToken | None

Classify a segment and apply gating logic.

Args: frame: A Frame object or 2D array of shape (segment_length, num_channels)

Returns: CommandToken if successful, else None.

predict_raw

def predict_raw(self, frame: Frame | np.ndarray) -> tuple[str, float, list[float]]

Classify a segment/frame without debounce gates.

Returns: (best_command, probability, all_probabilities)

subvocal.emg_core.ml.lisa

LISA LLM Integrated Scoring Adjustment per Benster et al. 2024.

Rescores beam-search hypotheses from an acoustic model (sEMG / ASR) using a frozen LLM as a language prior. Per MONA's LISA (arxiv:2403.05583), final score interpolates acoustic and LLM scores::

final = (1 - alpha) * acoustic + alpha * llm + beta * length_bonus

The LLM is accessed via :class:`subvocal.core.interfaces.LLMProvider`. If a provider exposes ``score(text)``, ``score_text(text)`` or similar, it is used; otherwise a deterministic heuristic fallback is applied.

References ---------- Benster et al., 2024 — ``MONA + LISA``. arxiv:2403.05583.

Guarded: torch is optional; missing torch only disables ``beam_search_rescore`` logits decoding (raises ``MissingDependencyError``). ``rescore`` and ``lisa_rescore`` work without torch.

Example ------- >>> from subvocal.core.llm_providers import HeuristicProvider >>> from subvocal.emg_core.ml.lisa import LISA, lisa_rescore >>> lisa = LISA(llm_provider=HeuristicProvider(), alpha=0.5) >>> reranked = lisa.rescore(["hello world", "hallo world"], [-0.2, -0.5])

class LISA(nn.Module)

LLM Integrated Scoring Adjustment (LISA).

Rescores acoustic beam-search hypotheses with a frozen LLM prior.

Args: llm_provider: LLM provider for scoring. May be any :class:`subvocal.core.interfaces.LLMProvider` or object with ``score(text)``. If ``None``, a deterministic heuristic is used (still satisfies the API). alpha: Interpolation weight for LLM score (0..1). ``0`` = acoustic only, ``1`` = LLM only. Default 0.5 per MONA. beta: Length-bonus weight (default 0.3). Added as ``beta * (len(text)/100)`` or word-count normalized; set 0 to disable.

__init__

def __init__(self, llm_provider: Any | None = None, alpha: float = 0.5, beta: float = 0.3) -> None

No description.

rescore

def rescore(self, hypotheses: list[str], beam_scores: list[float]) -> list[tuple[str, float]]

Rescore hypotheses by interpolating acoustic and LLM scores.

Computes for each hypothesis::

combined = (1 - alpha) * acoustic + alpha * llm_score + beta * length_norm

where ``length_norm`` is ``len(text)/100`` (character) to mildly favor longer hypotheses when ``beta>0`` (paper's length bonus).

Args: hypotheses: List of hypothesis strings from beam search. beam_scores: List of acoustic scores (log-probs or probs) parallel to hypotheses. Higher is better.

Returns: List of ``(hypothesis, combined_score)`` sorted descending by combined_score.

Raises: ValueError: if lengths mismatch or empty input.

forward

def forward(self, hypotheses: list[str], beam_scores: list[float]) -> list[tuple[str, float]]

Alias for :meth:`rescore` for nn.Module compatibility.

beam_search_rescore

def beam_search_rescore(self, logits: Any, vocab: list[str], beam_width: int = 10, lm_weight: float = 0.5) -> str

Decode logits with beam search then LISA-rescore.

Args: logits: Tensor of shape ``(T, V)`` or ``(B, T, V)`` (acoustic logits per frame). If batched, first batch is decoded. vocab: List of vocabulary tokens (length V). May include special tokens ``<blank>``, ``<pad>``, ``<eos>`` which are handled: ``<blank>`` is skipped (CTC), ``<pad>`` ignored. beam_width: Beam width (default 10). lm_weight: LLM interpolation weight for this call (overrides ``self.alpha`` for this decoding).

Returns: Top hypothesis string after LISA rescoring.

Raises: MissingDependencyError: if torch not available. ValueError: if shapes invalid.

class LISA

Stub — LISA scorer without torch (rescore works, beam_search requires torch).

__init__

def __init__(self, llm_provider: Any | None = None, alpha: float = 0.5, beta: float = 0.3) -> None

No description.

rescore

def rescore(self, hypotheses: list[str], beam_scores: list[float]) -> list[tuple[str, float]]

No description.

forward

def forward(self, hypotheses: list[str], beam_scores: list[float]) -> list[tuple[str, float]]

No description.

beam_search_rescore

def beam_search_rescore(self, logits: Any, vocab: list[str], beam_width: int = 10, lm_weight: float = 0.5) -> str

No description.

lisa_rescore

def lisa_rescore(hypotheses: list[dict[str, Any]], llm_provider: Any | None = None, weight: float = 0.5) -> list[dict[str, Any]]

Re-rank hypotheses list with LISA LLM scoring (functional API).

Functional wrapper around :class:`LISA` for dict-based beam outputs (common in ASR/EMG decoders). Each hypothesis dict must contain a text field and a score field; flexible key aliases are accepted.

Args: hypotheses: List of dicts, each with text and acoustic score. Accepted text keys: ``text``, ``hypothesis``, ``transcript``, ``hyp``, ``str``. Accepted score keys: ``score``, ``acoustic_score``, ``beam_score``, ``acoustic``, ``log_prob``. Additional keys are preserved in output. llm_provider: LLM provider for scoring. May be an :class:`subvocal.core.interfaces.LLMProvider`, a provider name string (e.g., ``"heuristic"``, ``"openai"``), or ``None`` for heuristic fallback. weight: LLM interpolation weight (0..1). ``0`` = acoustic only, ``1`` = LLM only. Default 0.5.

Returns: Re-ranked list of dicts sorted descending by combined score. Each dict is shallow-copied and augmented with ``llm_score``, ``combined_score`` (or ``lisa_score`` alias).

Raises: ValueError: if weight not in [0,1] or hypotheses not a list. TypeError: if hypotheses contains non-dict items.

Example: >>> from subvocal.core.llm_providers import HeuristicProvider >>> hyps = [{"text": "hello world", "score": -0.2}, {"text": "hallo world", "score": -0.5}] >>> reranked = lisa_rescore(hyps, HeuristicProvider(), weight=0.5) >>> reranked[0]["text"] 'hello world'

subvocal.emg_core.ml.model_io

Model I/O utilities for saving and loading classifiers.

get_model_path

def get_model_path(user_id: str, model_type: str = 'rf') -> str

Get the path to a user's model file.

Saves RF/SVM models as joblib, and CNN/GRU/Transformer models as .pth.

save_model

def save_model(model_data: dict[str, Any], user_id: str, model_type: str = 'rf') -> str

Save a model and its associated metadata to disk.

load_model

def load_model(user_id: str, model_type: str = 'rf') -> dict[str, Any]

Load a model and its metadata from disk.

model_exists

def model_exists(user_id: str, model_type: str = 'rf') -> bool

Check if a model exists for a user.

subvocal.emg_core.ml.mona

MONA cross-modal losses per Benster et al. 2024 (arxiv:2403.05583).

Implements cross-modal contrastive (crossCon) and supervised temporal contrastive (supTcon) losses with latent Dynamic Time Warping (DTW) alignment for silent EMG + vocal EMG + audio (LibriSpeech).

Reference --------- Benster et al., 2024 — ``An EMG-based Silent Speech Interface with Cross-Modal Contrastive, SupCon and DTW Alignment (MONA)``. Preprint arxiv:2403.05583.

Guarded: torch is optional; missing torch raises MissingDependencyError for tensor losses. DTW has a numpy fallback and works without torch. BatchSampler undersamples LibriSpeech to 50% per epoch (Algorithm 1).

class BatchSampler

Undersample LibriSpeech to 50% per epoch (MONA Algorithm 1).

Each epoch, retains 100% of EMG indices (silent + vocal) and a random 50% subset of LibriSpeech indices (without replacement), shuffles the combined pool, and yields batches.

This keeps EMG and LibriSpeech balanced per batch despite LibriSpeech being much larger (~960 h vs ~10 h EMG), matching MONA's ``undersample LS to 50% per epoch`` description.

Args: emg_indices: List of EMG sample indices (e.g., 0..N_emg-1) or int count, or None (inferred from dataset sizes). librispeech_indices: List of LibriSpeech sample indices or int count. batch_size: Batch size (default 32). librispeech_ratio: Fraction of LibriSpeech to keep per epoch (default 0.5 per MONA). shuffle: Whether to shuffle the combined pool each epoch. seed: Base random seed; epoch is added for per-epoch determinism. drop_last: Whether to drop last incomplete batch. dataset_size: Total dataset size (alternative to explicit indices). librispeech_size: Number of LibriSpeech samples (used with dataset_size). dataset: Optional dataset object (unused but accepted for API compat; if it exposes ``emg_indices`` / ``librispeech_indices`` attributes, they are used).

Example: ``sampler = BatchSampler(emg_indices=list(range(100)), librispeech_indices=list(range(100,600)), batch_size=32)`` ``for batch in sampler: train_step(batch)`` ``sampler.set_epoch(epoch)`` # call each epoch for new subsample

Notes: Compatible with ``torch.utils.data.DataLoader`` via manual batching or by passing ``batch_sampler=sampler``. When used with PyTorch, each iteration yields a ``list[int]`` batch of indices.

__init__

def __init__(self, emg_indices: list[int] | int | None = None, librispeech_indices: list[int] | int | None = None, batch_size: int = 32, librispeech_ratio: float = 0.5, shuffle: bool = True, seed: int | None = None, drop_last: bool = False, dataset_size: int | None = None, librispeech_size: int | None = None, dataset: Any | None = None, **kwargs: Any) -> None

No description.

set_epoch

def set_epoch(self, epoch: int) -> None

Set epoch for per-epoch subsampling determinism (call each epoch).

num_emg

def num_emg(self) -> int

No description.

num_librispeech

def num_librispeech(self) -> int

No description.

num_librispeech_sampled

def num_librispeech_sampled(self) -> int

Number of LibriSpeech samples kept per epoch (ratio).

get_combined_indices

def get_combined_indices(self) -> list[int]

Return current epoch's combined pool (for debugging).

cross_contrastive_loss

def cross_contrastive_loss(emg_latents: Any, audio_latents: Any, temperature: float = 0.1) -> Any

Cross-modal contrastive loss (crossCon) between EMG and audio latents.

Bidirectional InfoNCE: for batch size B, similarity matrix S[i,j] = cosine(emg_i, audio_j) / temperature. Positives are diagonal. Loss = (CE(S, labels) + CE(S^T, labels)) / 2.

Latents may be (B, D) or (B, T, D) (temporal mean-pooled).

Args: emg_latents: Tensor of shape (B, D) or (B, T, D). audio_latents: Tensor of shape (B, D) or (B, T, D). temperature: Softmax temperature, ``>0`` (default 0.1 per MONA).

Returns: Scalar Tensor loss (requires grad). 0 if batch size < 2 still differentiable.

Raises: MissingDependencyError: if torch not installed. ValueError: if shapes mismatched or temperature invalid.

supervised_temporal_contrastive_loss

def supervised_temporal_contrastive_loss(latents: Any, labels: Any, temperature: float = 0.1) -> Any

Supervised temporal contrastive loss (supTcon).

SupCon-style: positives share the same label, negatives differ. For each anchor i, loss pulls positives j (|labels_j==labels_i, j!=i) closer in latent space than negatives.

Temporal handling: if latents is (B, T, D), mean-pools over T. Labels may be (B,) or (B, 1).

Args: latents: Tensor (B, D) or (B, T, D). labels: Tensor (B,) long/int or array-like. temperature: Softmax temperature (>0, default 0.1).

Returns: Scalar Tensor loss. Returns 0 if no anchor has positives.

Raises: MissingDependencyError: if torch not installed.

dtw_align

def dtw_align(latents_a: Any, latents_b: Any) -> Any

Align ``latents_a`` to ``latents_b`` via Dynamic Time Warping in latent space.

Latent DTW per MONA: warps silent EMG (or vocal EMG) to audio temporal structure in the shared encoder latent space. Uses squared Euclidean distance in latent space; warps ``a`` to ``b``'s time axis by averaging frames of ``a`` that map to each frame of ``b`` along the optimal path.

Supports ``torch.Tensor`` (GPU/CPU) or ``np.ndarray``. If torch is not available, numpy fallback is used transparently. Batched inputs ``(B, T, D)`` are handled per-batch element.

Args: latents_a: Tensor/array of shape ``(T_a, D)`` or ``(B, T_a, D)`` — source to warp (e.g., silent EMG latents). latents_b: Tensor/array of shape ``(T_b, D)`` or ``(B, T_b, D)`` — target time axis (e.g., audio latents).

Returns: Warped version of ``latents_a`` aligned to ``latents_b`` time axis: * 2-D inputs ``(T_a,D)`` / ``(T_b,D)`` → ``(T_b, D)`` * 3-D batched ``(B, T_a,D)`` / ``(B, T_b,D)`` → ``(B, T_b, D)`` Returned type matches input type (Tensor if inputs were Tensor, otherwise ndarray). Device/dtype preserved for Tensor outputs.

Raises: ValueError: if feature dims mismatch or batch dims mismatch.

mona_loss

def mona_loss(emg_silent: Any, emg_vocal: Any, audio: Any, labels: Any, weights: dict[str, float] | None = None) -> dict[str, Any]

Combined MONA objective with crossCon, supTcon and CTC components.

Computes::

total = w_cross * crossCon + w_sup * supTcon + w_ctc * ctc

where crossCon is averaged over (silent↔audio) and (vocal↔audio) when both EMG modalities are present, supTcon is the supervised temporal contrastive loss on EMG latents (mean-pooled if temporal), and ctc is an optional CTC loss if logits are provided.

Args: emg_silent: Silent EMG latents Tensor (B, D) or (B, T, D) or None. emg_vocal: Vocal EMG latents Tensor (B, D) or (B, T, D) or None. audio: Audio (LibriSpeech) latents Tensor (B, D) or (B, T, D) or None. labels: Ground-truth labels Tensor (B,) or (B, L) for CTC; may be list/ndarray; None => supTcon/ctc skipped. weights: Dict with keys ``crossCon``/``cross``, ``supTcon``/``sup``, ``ctc``. Missing keys default to 1.0. Also accepts ``total`` not needed.

Returns: Dict with keys ``crossCon``, ``supTcon``, ``ctc``, ``total``. Values are scalar Tensors (if torch available) or floats (fallback). Always contains ``total``.

Raises: MissingDependencyError: if torch not installed and tensor inputs given (fallback returns float dict without raising, to keep training loop alive in non-torch tests).

subvocal.emg_core.ml.pretrain

Pre-training script for deep neural sEMG classifiers on public baseline datasets.

generate_synthetic_dataset

def generate_synthetic_dataset(user_id: str = 'pretrained', num_classes: int = 5, segments_per_class: int = 40)

Generate a reproducible multi-channel sEMG dataset to simulate a public dataset.

Each class is synthesized with unique harmonic and frequency patterns.

run_pretraining

def run_pretraining()

Run pre-training pipeline for all deep architectures.

subvocal.emg_core.ml.spd_gru

SPD-GRU CTC for silent-sEMG phoneme decoding.

Implements the SPD-GRU pipeline from:

* Gowda & Miller, "SPD-GRU: Riemannian sEMG to Phonemes", ACL 2025. * Gowda & Miller, Findings of ACL 2026 (SPD manifold + CTC). * Gowda et al., J Neural Eng 2024 (Riemannian sEMG features).

Architecture (ACL 2026, Fig. 3)::

SPD sequence (B,T,C,C) ──► logm via eigh ──► upper-tri flatten (C*(C+1)//2) ──► Linear(hidden) ──► 3-layer GRU ──► Linear(num_phonemes) ──► CTC logits (B,T,V)

The Riemannian map is performed **inside** the forward pass with :func:`torch.linalg.eigh` so gradients flow through the SPD→tangent projection (Gowda 2025, App. A). Off-diagonal ``sqrt(2)`` scaling is omitted to keep the projection linear and compatible with the logm flatten helper in :mod:`subvocal.emg_core.dsp.spd`.

class SPDGRU(nn.Module)

SPD-GRU CTC decoder.

Maps an SPD sequence to per-frame phoneme logits suitable for :class:`torch.nn.CTCLoss`.

Args: num_channels: Number of sEMG channels ``C`` (default 4). The Riemannian input dimension is ``K = C*(C+1)//2`` (e.g., 10). hidden_size: GRU hidden width (default 64, ACL 2026 Sec. 5). num_layers: Number of stacked GRU layers (default 3 per Gowda 2025/2026; dropout applied between layers). num_phonemes: Phoneme vocabulary size including CTC blank (default 40; blank is index 0, cf. Gowda 2025 Table 1). dropout: Dropout probability between GRU layers and before the final projection (default 0.2).

Input: ``spd_seq`` — Tensor ``(B,T,C,C)`` or ``(T,C,C)`` (single sequence). ``numpy`` arrays are auto-converted (detached CPU).

Output: Logits ``(B,T,num_phonemes)`` — raw, **not** log-softmaxed. Apply ``log_softmax(-1)`` and permute to ``(T,B,V)`` for :class:`torch.nn.CTCLoss`.

References: Gowda & Miller, ACL 2025 & Findings of ACL 2026; J Neural Eng 2024.

__init__

def __init__(self, num_channels: int = 4, hidden_size: int = 64, num_layers: int = 3, num_phonemes: int = 40, dropout: float = 0.2) -> None

No description.

forward

def forward(self, spd_seq: Any) -> Any

Forward pass.

Args: spd_seq: Tensor ``(B,T,C,C)`` or ``(T,C,C)``. ``numpy`` arrays of same shape are accepted and converted to ``torch.float32`` on the module's device.

Returns: Logits ``(B,T,num_phonemes)``.

class SPDGRU

Stub – raises MissingDependencyError when torch is absent.

__init__

def __init__(self, num_channels: int = 4, hidden_size: int = 64, num_layers: int = 3, num_phonemes: int = 40, dropout: float = 0.2) -> None

No description.

forward

def forward(self, spd_seq: Any) -> Any

No description.

ctc_loss

def ctc_loss(log_probs: Any, targets: Any, input_lengths: Any, target_lengths: Any, blank: int = 0, zero_infinity: bool = True) -> Any

CTC loss wrapper (log_probs ``(T,N,V)`` or logits ``(B,T,V)``).

Accepts both ``(T,N,V)`` log-probs (standard) and ``(B,T,V)`` logits (SPDGRU output) for convenience; orientation and logit vs log-prob are auto-detected.

Args: log_probs: Log-probabilities ``(T, N, V)`` or logits ``(B,T,V)``. If ``(B,T,V)`` is detected (via ``input_lengths``), it is converted via ``log_softmax`` and permuted to ``(T,B,V)``. targets: Concatenated target indices ``(sum(target_lengths),)``. input_lengths: Lengths ``(N,)`` of log_probs per batch element. target_lengths: Lengths ``(N,)`` of targets per batch element. blank: Blank label index (default 0 per Gowda 2025). zero_infinity: Whether to zero infinite losses (default True).

Returns: Scalar loss tensor.

References: Graves et al., 2006; Gowda 2025, Sec. 5.3 (CTC for phonemes).

ctc_loss_from_logits

def ctc_loss_from_logits(logits: Any, targets: Any, input_lengths: Any, target_lengths: Any, blank: int = 0, zero_infinity: bool = True) -> Any

Convenience: compute CTC loss directly from logits ``(B,T,V)`` or ``(T,C,C)`` style.

Args: logits: Raw logits ``(B,T,V)`` or ``(T,V)``. If ``(T,V)`` batch is 1. Also accepts SPDGRU logits ``(B,T,num_phonemes)``. targets: Concatenated targets ``(sum(target_lengths),)``. input_lengths: ``(N,)`` or scalar. target_lengths: ``(N,)`` or scalar. blank: Blank index. zero_infinity: Zero infinities.

Returns: Scalar loss.

greedy_decode

def greedy_decode(logits: Any, blank_id: int = 0, collapse_repeated: bool = True) -> list[list[int]]

Greedy CTC decoding (argmax + collapse).

Args: logits: Tensor ``(B,T,V)`` or ``(T,V)`` or ``(T,C,C)``-style? Actually expects phoneme logits. Can also be ``(B,T,V)`` numpy. blank_id: CTC blank index (default 0). collapse_repeated: Whether to collapse repeated non-blank tokens.

Returns: List of length ``B`` (or 1) with decoded index sequences (blanks removed).

References: Graves et al., CTC greedy search; Gowda 2025, Sec. 5.4.

train_step

def train_step(model: Any, optimizer: Any, logits: Any, targets: Any, input_lengths: Any, target_lengths: Any, blank: int = 0) -> Any

Single CTC training step (forward → loss → backward → step).

Args: model: :class:`SPDGRU` instance (unused except for zero_grad validation; logits may be pre-computed). optimizer: Torch optimizer (e.g., ``AdamW``). logits: Raw logits ``(B,T,V)`` from ``model(spd_seq)``. targets: Concatenated target ``(sum(target_lengths),)``. input_lengths: ``(N,)`` input lengths. target_lengths: ``(N,)`` target lengths. blank: CTC blank index.

Returns: Scalar loss tensor (detached for logging).

References: Gowda 2025, Sec. 6 (AdamW, CTC training).

train_spd_gru

def train_spd_gru(model: Any, optimizer: Any, spd_seq: Any, targets: Any, input_lengths: Any, target_lengths: Any, blank: int = 0) -> Any

End-to-end training helper: ``spd_seq → logits → CTC loss → step``.

Args: model: :class:`SPDGRU`. optimizer: Torch optimizer. spd_seq: SPD sequence ``(B,T,C,C)`` or ``(T,C,C)``, Tensor or ndarray. targets: Concatenated phoneme targets. input_lengths: Input lengths ``(N,)``. target_lengths: Target lengths ``(N,)``. blank: Blank index.

Returns: Tuple ``(loss, logits)`` where ``loss`` is detached scalar and ``logits`` are ``(B,T,V)``.

References: Gowda 2025, Sec. 6.

subvocal.emg_core.ml.speechnet

SpeechNet tiny CNN per SilentWear (Spacone et al. 2026, ETH Zurich).

15k-parameter depthwise-separable CNN for 8 commands + rest, tailored for GAP9 MCU (63.9 uJ / inference). Input ``(B, C, T)`` with C=14 (SilentWear) or 4-8 (generic), T=150 (1.5 s @ 100 Hz or 150 ms @ 1000 Hz window).

Architecture ------------ 3 depthwise-separable conv blocks (depthwise Conv1d groups + pointwise 1x1), each with BatchNorm, ReLU, MaxPool(2), Dropout, followed by AdaptiveAvgPool1d + linear head. ~15k params for C=4..14.

Reference --------- Spacone et al. 2026, SilentWear, ETH Zurich — GAP9 deployment, inter-session fine-tuning with <10 min calibration data.

Guarded: torch is optional; missing torch raises MissingDependencyError.

class SpeechNet(nn.Module)

Tiny depthwise-separable CNN (~15k params) for sEMG commands.

Args: num_channels: EMG channels (14 for SilentWear, 4-8 generic). num_classes: Output classes (8 commands + rest if needed). segment_length: Temporal length T (default 150). dropout: Dropout prob between blocks.

__init__

def __init__(self, num_channels: int = 4, num_classes: int = 8, segment_length: int = 150, dropout: float = 0.2) -> None

No description.

forward

def forward(self, x: Any) -> Any

Forward pass.

Args: x: Tensor of shape (B, C, T).

Returns: Logits of shape (B, num_classes).

count_parameters

def count_parameters(self) -> int

Return total parameter count (~15k, including frozen).

count_trainable_parameters

def count_trainable_parameters(self) -> int

Return trainable parameter count (after freezing backbone).

count_parameters_total

def count_parameters_total(self) -> int

Alias for count_parameters().

estimate_energy

def estimate_energy(self) -> float

Energy per inference in microjoules.

Returns paper's GAP9 measurement 63.9 uJ, scaled slightly for channel count (linear with first pointwise).

estimate_energy_uj

def estimate_energy_uj(self) -> float

Alias for estimate_energy().

estimate_latency

def estimate_latency(self) -> float

Latency per inference in milliseconds (GAP9).

estimate_latency_ms

def estimate_latency_ms(self) -> float

Alias for estimate_latency().

estimate_flops

def estimate_flops(self) -> int

Estimate FLOPs per forward pass.

class SpeechNet

Stub — raises MissingDependencyError when torch is absent.

__init__

def __init__(self, num_channels: int = 4, num_classes: int = 8, segment_length: int = 150, dropout: float = 0.2) -> None

No description.

forward

def forward(self, x: Any) -> Any

No description.

count_parameters

def count_parameters(self) -> int

No description.

estimate_energy

def estimate_energy(self) -> float

No description.

estimate_latency

def estimate_latency(self) -> float

No description.

finetune_inter_session

def finetune_inter_session(model: Any, train_loader: Any, val_loader: Any | None = None, epochs: int = 5, lr: float = 0.0005, weight_decay: float = 0.0001, freeze_backbone: bool = True, device: str = 'cpu') -> dict[str, Any]

Incremental fine-tuning for a new session (<10 min data).

SilentWear protocol: freeze depthwise-separable backbone, fine-tune only the linear head (or last block + head) with low LR for few epochs, using calibration data collected in <10 min (~50-150 utterances).

Args: model: SpeechNet instance (or any nn.Module with .classifier). train_loader: DataLoader yielding (B, C, T), labels. val_loader: Optional validation loader. epochs: Few epochs (3-10, default 5) to avoid overfit on tiny set. lr: Low learning rate (1e-4 - 1e-3). weight_decay: Weight decay. freeze_backbone: If True, freeze blocks 1-3, train head only. device: Torch device.

Returns: Dict with train loss history and val accuracy if available.

train_speechnet

def train_speechnet(num_channels: int = 4, num_classes: int = 8, segment_length: int = 150, train_data: Any | None = None, train_labels: Any | None = None, epochs: int = 20, batch_size: int = 16, lr: float = 0.001, weight_decay: float = 0.0001, device: str = 'cpu', seed: int = 42) -> tuple[Any, dict[str, Any]]

Train SpeechNet from scratch (convenience wrapper).

If train_data/train_labels are None, returns untrained model. Otherwise builds a DataLoader and trains with AdamW + CrossEntropy.

Returns: (model, history_dict)

subvocal.emg_core.ml.train

Training pipeline for sEMG command classifiers.

Supports: 1. RandomForest (Standard ML baseline using TD10 aggregated features). 2. SVM (Feature-based Support Vector Machine baseline). 3. 1D CNN (Deep learning model operating on raw time-series segments). 4. GRU (Bidirectional temporal RNN operating on raw time-series segments). 5. Transformer (Attention-based sequential encoder operating on raw segments).

class EMG1DCNN(nn.Module)

1D CNN for sEMG segment classification.

Applies convolutions over the temporal dimension of raw multichannel sEMG.

__init__

def __init__(self, num_channels: int, num_classes: int, segment_length: int = 150)

No description.

forward

def forward(self, x)

No description.

class EMGGRU(nn.Module)

Bidirectional GRU for temporal tracking of sEMG gestures.

__init__

def __init__(self, num_channels: int, num_classes: int, hidden_size: int = 64, num_layers: int = 2)

No description.

forward

def forward(self, x)

No description.

class EMGTransformer(nn.Module)

Small Transformer encoder for raw multichannel sEMG sequence classification.

__init__

def __init__(self, num_channels: int, num_classes: int, segment_length: int = 150, d_model: int = 64, nhead: int = 4, num_layers: int = 2, dim_feedforward: int = 128)

No description.

forward

def forward(self, x)

No description.

load_dataset

def load_dataset(user_id: str) -> tuple[list[np.ndarray], list[str]]

Load the calibration dataset for a user.

Returns: segments: list of raw 2D arrays, each (segment_length, num_channels) labels: list of label strings corresponding to each segment

preprocess_segments

def preprocess_segments(segments: list[np.ndarray], fs: float, low: float, high: float, notch_freq: float = config.NOTCH_FREQ, notch_q: float = config.NOTCH_Q) -> np.ndarray

Preprocess raw segments and stack into a 3D numpy array of shape (N, C, T).

train_model

def train_model(user_id: str, model_type: str = 'rf', test_size: float = 0.2, config_obj: TrainingConfig | None = None) -> dict[str, Any]

Train a sEMG model for a user and save it to disk.

Args: user_id: The ID of the user. model_type: "rf", "svm", "cnn", "gru", or "transformer". test_size: Split ratio for testing/validation. config_obj: Optional TrainingConfig parameters object.

Returns: A dictionary containing training metrics and results.

calibrate_model

def calibrate_model(user_id: str, pretrained_model_type: str, calibration_config: TrainingConfig | None = None) -> dict[str, Any]

Calibrate / fine-tune a pre-trained model for a new user.

For PyTorch models (CNN, GRU, Transformer), it loads pre-trained weights, replaces/adjusts the classification head to match the user's classes, freezes base layers (optionally), and fine-tunes on user calibration data. For classical models (RF, SVM), it fits a new pipeline on user data.

subvocal.mcp.server

Model Context Protocol (MCP) reference server for Subvocal Middleware.

class MockContextProvider(ContextProvider)

Exposes mock system context state.

get_context

def get_context(self) -> UserContext

No description.

get_provider_name

def get_provider_name(self) -> str

No description.

class MockActionExecutor(ActionExecutor)

Executes actions and logs execution history.

__init__

def __init__(self)

No description.

execute

def execute(self, action: Action) -> Any

No description.

can_execute

def can_execute(self, action: Action) -> bool

No description.

class SubvocalMCPServer

Zero-dependency Model Context Protocol server communicating via stdio.

__init__

def __init__(self)

No description.

handle_request

def handle_request(self, req: dict[str, Any]) -> dict[str, Any] | None

Process an incoming JSON-RPC 2.0 request and return the response dictionary.

run

def run(self)

Standard input/output reader loop.

main

def main() -> None

Console entry point for the ``subvocal-mcp`` stdio server.

subvocal.shorthand.decoder

Hybrid decoder for subvocal compressed shorthand.

Combines an articulatory-distance heuristic alignment algorithm (dynamic programming) with a model-agnostic LLM reconstruction layer.

articulatory_distance

def articulatory_distance(word1: str, word2: str) -> float

Calculates Levenshtein distance customized for sEMG articulatory phonetic groups.

Substitutions between letters in the same articulatory group cost 0.25 (highly likely sEMG error). Substitutions across different articulatory groups cost 1.0. Deletion in candidate (extra character in query) costs 1.0. Insertion in candidate (skipped character in query shorthand) costs 0.4.

find_best_shorthand_match

def find_best_shorthand_match(noisy_token: str, candidate_words: list[str]) -> list[tuple[str, float]]

Scores a noisy shorthand token against a list of candidate words.

Compresses candidates to shorthand and computes articulatory distance. Returns a list of tuples (candidate, distance) sorted by distance.

heuristic_decode_phrase

def heuristic_decode_phrase(noisy_phrase: str, web_context_words: list[str] | None = None, calendar_words: list[str] | None = None, contacts_words: list[str] | None = None, ui_elements: list[str] | None = None, contacts: list[str] | None = None, calendar_events: list[str] | None = None) -> tuple[str, float]

Decodes a full noisy shorthand phrase using articulatory heuristics.

Uses Command-Aware Context Prioritization and Phrase-Level Matching. If structured contexts are provided, they are prioritized based on the command. Otherwise, falls back to flat context words.

call_llm_api

def call_llm_api(provider: str, api_key: str, model: str | None, prompt: str) -> str | None

Helper to perform model-agnostic LLM calls via urllib without external SDK dependencies.

reconstruct_intent_llm

def reconstruct_intent_llm(noisy_input: str, heuristic_candidate: str, web_context: str | None = None, calendar: str | None = None, contacts: str | None = None, history: str | None = None) -> str | None

Reconstructs the target command phrase from noisy shorthand using a model-agnostic LLM.

Discovers API keys dynamically from the environment.

hybrid_decode

def hybrid_decode(noisy_phrase: str, web_context_words: list[str] | None = None, calendar_words: list[str] | None = None, contacts_words: list[str] | None = None, history: str | None = None, ui_elements: list[str] | None = None, contacts: list[str] | None = None, calendar_events: list[str] | None = None) -> tuple[str, float, str]

Combines heuristic alignment and LLM disambiguation for premium accuracy.

Returns: A tuple of (decoded_phrase, confidence, method_used) Method used is either "heuristic" or "llm".

subvocal.shorthand.simulator

Noisy channel simulator for subvocal compressed shorthand.

Converts target command phrases to compressed shorthand and applies realistic articulatory sEMG noise (substitutions, insertions, and deletions).

get_articulatory_substitution

def get_articulatory_substitution(char: str) -> str

Get a random alternative character from the same articulatory group to simulate sEMG confusion.

simulate_emg_noise

def simulate_emg_noise(shorthand_word: str, sub_rate: float = 0.1, del_rate: float = 0.05, ins_rate: float = 0.05) -> str

Applies deletion, insertion, and substitution noise to a shorthand word.

- Substitution: Replaces char with another from the same articulatory group (sEMG confusion). - Deletion: Drops the character (packet drop / weak signal). - Insertion: Inserts a random character (muscle twitch / swallow artifact).

phrase_to_noisy_shorthand

def phrase_to_noisy_shorthand(phrase: str, sub_rate: float = 0.1, del_rate: float = 0.05, ins_rate: float = 0.05) -> tuple[str, str]

Convert a full phrase to clean shorthand and simulated noisy shorthand.

Returns: A tuple of (clean_shorthand_phrase, noisy_shorthand_phrase)

subvocal.shorthand.spec

Shorthand and phonetic specifications for the subvocal interface.

Defines the articulatory phonetic groups representing sEMG jaw/throat activation clusters, and compression rules to construct the shorthand representation.

compress_word

def compress_word(word: str) -> str

Compress a single English word into its skeletal shorthand format.

Rules: 1. Lowercase the word. 2. Check if the word has a common abbreviation. 3. Otherwise: a. Keep the first letter. b. Remove subsequent vowels (a, e, i, o, u, y) and 'h'. c. Deduplicate consecutive identical consonants (e.g. 'gg' -> 'g').

word_to_articulatory_sequence

def word_to_articulatory_sequence(shorthand_word: str) -> list[str]

Convert a shorthand word to its articulatory group sequence.

Non-alphabet characters (like numbers and punctuation) are preserved as-is.

subvocal.shorthand.vocab

Target command vocabulary for the Subvocal interface.

Defines a phonetically diverse set of 17 commands optimized for throat/jaw surface Electromyography (sEMG) classification.

get_command_list

def get_command_list() -> list[str]

Return a list of all command names in the vocabulary.

get_command_details

def get_command_details(name: str) -> dict[str, Any]

Retrieve details for a specific command.

subvocal.tts.engine

Text-to-Speech (TTS) engine for subvocal feedback.

Supports native macOS 'say' / 'afplay' utilities, OpenAI Audio API, and pyttsx3.

class TTSEngine

Multi-backend Text-to-Speech generator and player.

__init__

def __init__(self, config: TTSConfig | None = None)

No description.

speak

def speak(self, text: str, filename: str | None = None) -> str

Synthesize and speak the text, saving the audio to a file.

Args: text: The plain text statement to speak. filename: Target file name (optional). If omitted, a file name is auto-generated based on text hash. Returns: The absolute path of the generated audio file.

subvocal.tts.schema

Pydantic schemas for the Text-to-Speech (TTS) engine.

Defines voices, speeds, formats, and configurations for audio feedback.

class TTSConfig(BaseModel)

Configuration settings for text-to-speech audio feedback.

validate_audio_format

def validate_audio_format(cls, v: str) -> str

No description.

validate_output_dir

def validate_output_dir(cls, v: str) -> str

No description.

subvocal.routing.selector

class WorkerNode(Protocol)

Protocol matching standard attributes of routing nodes.

id

def id(self) -> str

No description.

load

def load(self) -> float

No description.

status

def status(self) -> str

No description.

cpu_usage

def cpu_usage(self) -> float

No description.

class NodeSelector(ABC)

Base interface for all worker node routing selectors.

select_node

def select_node(self, nodes: list[WorkerNode]) -> WorkerNode

Selects the best node from candidate worker nodes.

Args: nodes: Candidates list.

Returns: The chosen WorkerNode.

Raises: ValueError: If nodes list is empty.

class SessionCountSelector(NodeSelector)

Selects the worker node with the minimum active session load.

select_node

def select_node(self, nodes: list[WorkerNode]) -> WorkerNode

No description.

class CPULoadSelector(NodeSelector)

Selects the worker node with the lowest CPU load report.

select_node

def select_node(self, nodes: list[WorkerNode]) -> WorkerNode

No description.

subvocal.runtime.egress

class EgressManager

Coordinates downstream data exports, JSONL tracing, and audio speech feedback.

__init__

def __init__(self, trace_path: str | None = None, tts_engine: Any | None = None)

No description.

speak

def speak(self, text: str) -> None

Plays speech feedback to the user via TTS.

write_trace

def write_trace(self, trace_entry: dict) -> None

Appends a trace entry to the pipeline trace log file.

record_signal

def record_signal(self, output_path: str, frames: list[Any]) -> None

Saves raw biometric frames to a JSON document for training/fine-tuning models.

subvocal.runtime.ingress

class IngressManager

Manages active hardware source registration, monitoring, and failovers.

__init__

def __init__(self)

No description.

register_source

def register_source(self, name: str, source: HardwareSource, is_fallback: bool = False) -> None

Registers a biometric input source.

start

def start(self) -> None

Starts the active ingress source stream.

stop

def stop(self) -> None

Stops all registered ingress sources.

active_name

def active_name(self) -> str | None

No description.

get_active_source

def get_active_source(self) -> HardwareSource | None

No description.

trigger_failover

def trigger_failover(self) -> None

Fails over dynamically to the registered fallback source.

subvocal.runtime.session

class Session

Represents an active silent speech processing session.

Co-ordinates real-time signal level monitoring, stream tracking, quality scoring, and intent decoding on a background OpsQueue thread.

__init__

def __init__(self, id: str, config: SubvocalConfig, hardware: HardwareSource, classify_fn: Callable[[Frame], CommandToken | None], llm_provider: LLMProvider, context_provider: ContextProvider, executor: ActionExecutor, policy_engine: Any | None = None, trace_path: str | None = None, on_state_changed: Callable[[str], None] | None = None, on_token: Callable[[CommandToken], None] | None = None, on_intent: Callable[[Intent], None] | None = None, on_action: Callable[[Action, str], None] | None = None, on_error: Callable[[Exception], None] | None = None, telemetry: Any | None = None, data_channel: Any | None = None)

No description.

state

def state(self) -> str

Returns the current state of the session state machine.

token_buffer

def token_buffer(self) -> list[CommandToken]

Returns the raw reference to the accumulated token buffer.

start

def start(self) -> None

Initializes and starts the session.

stop

def stop(self) -> None

Gracefully stops and closes the session.

push_frame

def push_frame(self, frame: Frame) -> None

Ingests a new Frame, running stream checks and classification asynchronously on OpsQueue.

process_phrase

def process_phrase(self) -> Action | None

Forces immediate decoding of the current token buffer and runs the resolved action.

subvocal.runtime.store

class SessionStore(ABC)

Abstract interface for session configuration and state storage.

save_session

def save_session(self, session_id: str, state: str, config_yaml: str, metadata: dict[str, Any]) -> None

Saves or updates a session configuration and state snapshot.

get_session

def get_session(self, session_id: str) -> dict[str, Any] | None

Retrieves a session snapshot by ID, returning metadata, config, and state.

delete_session

def delete_session(self, session_id: str) -> None

Deletes a session snapshot by ID.

list_sessions

def list_sessions(self) -> list[dict[str, Any]]

Lists all stored sessions.

class InMemorySessionStore(SessionStore)

In-memory session registry for lightweight, transient deployments.

__init__

def __init__(self)

No description.

save_session

def save_session(self, session_id: str, state: str, config_yaml: str, metadata: dict[str, Any]) -> None

No description.

get_session

def get_session(self, session_id: str) -> dict[str, Any] | None

No description.

delete_session

def delete_session(self, session_id: str) -> None

No description.

list_sessions

def list_sessions(self) -> list[dict[str, Any]]

No description.

class SQLiteSessionStore(SessionStore)

SQLite-backed session registry for persistent edge configurations.

__init__

def __init__(self, db_path: str)

No description.

save_session

def save_session(self, session_id: str, state: str, config_yaml: str, metadata: dict[str, Any]) -> None

No description.

get_session

def get_session(self, session_id: str) -> dict[str, Any] | None

No description.

delete_session

def delete_session(self, session_id: str) -> None

No description.

list_sessions

def list_sessions(self) -> list[dict[str, Any]]

No description.

subvocal.runtime.worker

class SessionWorker

Manages active sessions and load-tracking across a pool of silent speech sessions.

Exposes ``id``, ``load``, ``cpu_usage``, and ``status`` so an instance satisfies the routing ``WorkerNode`` protocol and can be ranked by the selectors in :mod:`subvocal.routing`. Equivalent to LiveKit's SessionWorker/AgentWorker.

__init__

def __init__(self, config: SubvocalConfig, max_sessions: int = 10, store: Any | None = None, worker_id: str | None = None)

No description.

load

def load(self) -> float

Returns the current load fraction of the worker (0.0 to 1.0).

cpu_usage

def cpu_usage(self) -> float

Returns host CPU utilization percentage (0.0 if psutil is unavailable).

Lets a SessionWorker be ranked by :class:`subvocal.routing.CPULoadSelector`. Handles psutil.cpu_percent(interval=None) first-call 0.0 by priming in __init__ and, if still 0.0 on first real call, retrying with a tiny interval fallback would block – so we prime instead and document the limitation.

status

def status(self) -> str

Returns status string based on load.

create_session

def create_session(self, session_id: str, *args, **kwargs) -> Session

Creates, registers, and returns a new Session.

Raises ValueError if session pool capacity is exhausted.

get_session

def get_session(self, session_id: str) -> Session | None

Retrieves a registered Session by ID, or None if not found.

remove_session

def remove_session(self, session_id: str) -> Session | None

Stops and unregisters a Session by ID.

start_session

def start_session(self, session_id: str) -> None

Starts a registered session by ID.

close

def close(self) -> None

Gracefully closes all sessions and stops the worker.

subvocal.stream.buffer

class FrameRing

Thread-safe circular queue (ring buffer) storing biometric Frame segments.

__init__

def __init__(self, max_size: int = 100)

No description.

push

def push(self, frame: Frame) -> None

Pushes a new Frame onto the ring buffer, evicting the oldest if full.

pop

def pop(self) -> Frame | None

Pops and returns the oldest Frame from the ring buffer, or None if empty.

clear

def clear(self) -> None

Clears the ring buffer.

get_all

def get_all(self) -> list[Frame]

Returns a copy of all current frames in chronological order.

class StreamStats

Windowed tracking for frame arrivals, sample count, gaps, and inter-frame arrival jitter.

__init__

def __init__(self)

No description.

observe

def observe(self, frame: Frame) -> None

Observes a new frame and updates statistics.

get_stats

def get_stats(self) -> dict[str, Any]

Returns a snapshot of the current stats.

reset

def reset(self) -> None

Resets all stats to zero.

subvocal.stream.datachannel

class BiometricDataChannelServer

TCP server that broadcasts real-time biometric metrics to connected dashboard/client nodes.

__init__

def __init__(self, host: str = '127.0.0.1', port: int = 8100)

No description.

start

def start(self) -> None

Starts the TCP server in a background thread.

broadcast

def broadcast(self, payload: dict[str, Any]) -> None

Sends a JSON-serialized dictionary to all active listeners.

close

def close(self) -> None

Closes the server and drops all client connections.

class BiometricDataChannelClient

Helper client to connect to the BiometricDataChannelServer and parse message frames.

__init__

def __init__(self, host: str = '127.0.0.1', port: int = 8100)

No description.

connect

def connect(self) -> None

Connects to the server socket.

read_messages

def read_messages(self)

Generator yielding parsed messages from the streaming buffer.

close

def close(self) -> None

Disconnects the client socket.

subvocal.stream.level

class SignalLevel

Tracks and smooths the biological/electrical signal level of sEMG channels.

Equivalent to LiveKit's AudioLevel.

__init__

def __init__(self, active_level: float = 0.1, min_percentile: float = 40.0, update_interval_ms: int = 400, smooth_intervals: int = 2)

No description.

observe

def observe(self, frame: Frame) -> None

Observes a new Frame, computes its MAV, and updates sliding window metrics.

get_level

def get_level(self, now: float) -> tuple[float, bool]

Returns the current smoothed signal level and active status.

Resets level to 0 if signal is stale (no updates received for 2x update_interval).

subvocal.stream.quality

class SignalQualityScorer

Scores sEMG signal quality using physical metrics (saturation, drift, dropouts, SNR).

Outputs a MOS-like score mapped to EXCELLENT, GOOD, POOR, and LOST states. Equivalent to LiveKit's connectionquality Scorer.

__init__

def __init__(self, clip_max: float = 5.0, increase_factor: float = 0.4, decrease_factor: float = 0.8)

No description.

score

def score(self) -> float

Returns the current MOS-like quality score (1.0 - 4.5).

quality

def quality(self) -> str

Returns the current mapped quality state.

on_status_changed

def on_status_changed(self, callback: Callable[[str], None]) -> None

Registers a callback to be run on quality state transitions.

update

def update(self, frame: Frame) -> None

Evaluates a Frame, computes metrics, updates the quality score, and checks transitions.

subvocal.stream.tracker

class StreamTracker

Monitors whether a biometric sEMG stream is active or stopped.

Applies a hysteresis threshold to prevent bouncing between states. Equivalent to LiveKit's StreamTracker.

__init__

def __init__(self, samples_required: int = 3, cycles_required: int = 5)

No description.

status

def status(self) -> str

Returns the current status (active/stopped).

generation

def generation(self) -> int

Returns the current tracker worker generation.

on_status_changed

def on_status_changed(self, callback: Callable[[str], None]) -> None

Registers a callback to execute on status transitions.

observe

def observe(self, has_activity: bool) -> None

Observes an activity tick and updates status state using hysteresis.

set_paused

def set_paused(self, paused: bool) -> None

Pauses or resumes tracking.

Increments generation to invalidate older checks.

reset

def reset(self) -> None

Resets the tracker to initial stopped state.

stop

def stop(self) -> None

Permanently stops the tracker.

subvocal.auth.grants

class ActionGrants(BaseModel)

Capability-scoped credentials detailing authorized actions and constraints.

class GrantsPolicy(AuthorizationPolicy)

Enforces capability-scoped authorization checks on proposed Actions.

is_authorized

def is_authorized(self, action: Action, context: UserContext) -> bool

No description.

set_context_grants

def set_context_grants(grants: ActionGrants) -> contextvars.Token

Sets the given ActionGrants in the current thread/coroutine execution context.

get_context_grants

def get_context_grants() -> ActionGrants | None

Retrieves active ActionGrants from the current context.

clear_context_grants

def clear_context_grants(token: contextvars.Token) -> None

Resets the context grants to their previous state.

generate_token

def generate_token(grants: ActionGrants, secret_key: str, expires_in: int = 3600) -> str

Generates a secure HMAC-signed base64 token carrying capability claims.

verify_token

def verify_token(token: str, secret_key: str) -> ActionGrants | None

Verifies the HMAC signature and expiration of a base64 token, returning decoded ActionGrants.

ensure_permission

def ensure_permission(action_type: str, confidence: float = 1.0) -> None

Checks the active context grants and raises a PermissionError if the action is unauthorized.

subvocal.telemetry.service

class TelemetryService

Interface for telemetry exporting, based on LiveKit telemetry service design.

session_started

def session_started(self, session_id: str, config: SubvocalConfig) -> None

No description.

session_ended

def session_ended(self, session_id: str, stats: Any) -> None

No description.

phrase_detected

def phrase_detected(self, session_id: str, phrase_id: str, duration_seconds: float) -> None

No description.

intent_resolved

def intent_resolved(self, session_id: str, phrase_id: str, intent_name: str, confidence: float) -> None

No description.

action_executed

def action_executed(self, session_id: str, action_type: str, status: str) -> None

No description.

action_blocked

def action_blocked(self, session_id: str, action_type: str, reason: str) -> None

No description.

provider_retry

def provider_retry(self, session_id: str, provider_name: str, attempt: int, error: str) -> None

No description.

quality_changed

def quality_changed(self, session_id: str, quality_score: float, quality_state: str) -> None

No description.

error_occurred

def error_occurred(self, session_id: str, error_type: str) -> None

No description.

class NullTelemetry(TelemetryService)

Default telemetry implementation that does nothing.

class PrometheusTelemetry(TelemetryService)

Telemetry service that exports metrics to Prometheus.

Prometheus cardinality: session_id is high cardinality (unbounded, one series per session). To keep series count bounded (Prometheus recommends <10k series per metric, ideally <100k total), session_id is NOT used as a Prometheus label. Metrics are aggregated globally or by low-cardinality labels (intent, action_type, etc.). If per-session debugging is required, use logs or hash session_id via _hash_session_id() into a small bucket, but do not expose raw session_id. Documented limit: if you must add session_id, hash and cap distinct values <1000 and ensure series are removed on session_ended to avoid leak.

__init__

def __init__(self, config: SubvocalConfig)

No description.

session_started

def session_started(self, session_id: str, config: SubvocalConfig) -> None

No description.

session_ended

def session_ended(self, session_id: str, stats: Any) -> None

No description.

phrase_detected

def phrase_detected(self, session_id: str, phrase_id: str, duration_seconds: float) -> None

No description.

intent_resolved

def intent_resolved(self, session_id: str, phrase_id: str, intent_name: str, confidence: float) -> None

No description.

action_executed

def action_executed(self, session_id: str, action_type: str, status: str) -> None

No description.

action_blocked

def action_blocked(self, session_id: str, action_type: str, reason: str) -> None

No description.

provider_retry

def provider_retry(self, session_id: str, provider_name: str, attempt: int, error: str) -> None

No description.

quality_changed

def quality_changed(self, session_id: str, quality_score: float, quality_state: str) -> None

No description.

error_occurred

def error_occurred(self, session_id: str, error_type: str) -> None

No description.

shutdown

def shutdown(self) -> None

Release Prometheus port from registry (fixes _started_ports leak).

subvocal.utils.concurrency

class OpsQueue

A thread-safe serialized work queue running on a single background worker thread.

Equivalent to LiveKit's OpsQueue.

__init__

def __init__(self, name: str, min_size: int = 128, maxsize: int | None = None, flush_on_stop: bool = False, custom_logger: logging.Logger | None = None)

No description.

start

def start(self) -> None

Starts the background processing thread if not already started.

stop

def stop(self) -> None

Stops the queue.

If flush_on_stop is True, all currently queued operations are completed before the processing thread exits.

enqueue

def enqueue(self, fn: Callable, *args, **kwargs) -> bool

Enqueues an operation to be run on the background processing thread.

Returns False if the queue is stopped and the operation was not enqueued, or if the bounded queue is full and the item was dropped.

qsize

def qsize(self) -> int

Return approximate queue size (for monitoring/backpressure).

is_full

def is_full(self) -> bool

Return True if bounded queue is at capacity.

class IncrementalDispatcher

Allows multiple consumers to consume items thread-safely as they become available.

Producers can add items at any time. Equivalent to LiveKit's IncrementalDispatcher.

__init__

def __init__(self)

No description.

add

def add(self, item) -> None

Appends an item and broadcasts to waiting consumers.

done

def done(self) -> None

Signals that no more items will be added and wakes up all waiting consumers.

for_each

def for_each(self, callback: Callable) -> None

Iterates over all items (including future ones) and invokes the callback on them.

class ChangeNotifier

Registry for keyed observers to receive change notifications asynchronously.

Equivalent to LiveKit's ChangeNotifier.

__init__

def __init__(self)

No description.

add_observer

def add_observer(self, key: str, on_changed: Callable[[], None]) -> None

Registers a callback with a unique key.

remove_observer

def remove_observer(self, key: str) -> None

Unregisters a callback by its key.

has_observers

def has_observers(self) -> bool

Returns True if there is at least one observer registered.

notify_changed

def notify_changed(self) -> None

Asynchronously dispatches notifications to all registered observers.

class ChangeNotifierManager

Manages a registry of named ChangeNotifier instances.

Equivalent to LiveKit's ChangeNotifierManager.

__init__

def __init__(self)

No description.

get_notifier

def get_notifier(self, key: str) -> ChangeNotifier | None

Retrieves a notifier by key, or returns None if it doesn't exist.

get_or_create_notifier

def get_or_create_notifier(self, key: str) -> ChangeNotifier

Retrieves a notifier by key, creating a new one if it doesn't exist.

remove_notifier

def remove_notifier(self, key: str, force: bool = False) -> None

Removes a notifier by key.

If force is False, the notifier will only be removed if it has no observers.

class Debouncer

Resettable delay timer to debounce fast-firing events.

Equivalent to LiveKit's Debouncer.

__init__

def __init__(self, after_seconds: float)

No description.

add

def add(self, fn: Callable[[], None]) -> None

Cancels any pending callback timer and schedules a new one to execute fn.

set_duration

def set_duration(self, after_seconds: float) -> None

Updates the delay duration for subsequent debounce invocations.

cancel

def cancel(self) -> None

Cancels any pending timer execution.