Skip to content

Python API reference

One-import Python API: from tokeye import TokEye.

Gradio-free, so it is safe to embed in headless programs. TokEye() loads the default model (auto-downloading it from Hugging Face on first use)::

from tokeye import TokEye

eye = TokEye()
seg = eye.segment(signal, fs=500e3)  # Segmentation: mask + spectrogram + axes
mask = eye(signal)                   # just the (2, H, W) mask

TokEye

A loaded TokEye segmentation model plus its preprocessing settings.

Parameters:

Name Type Description Default
model str | Path

Registry name (downloaded and cached on first use) or a path to a local .pt/.pt2 checkpoint. Instance models such as ae_tf_maskrcnn are rejected; use tokeye alfvenspec.

DEFAULT_MODEL
device str

"auto" (CUDA, then MPS, then CPU), "cpu", "cuda", "cuda:N" or "mps". An unknown or unavailable device raises ValueError.

'auto'
n_fft int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
hop int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
clip_dc int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
clip_low int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
clip_high int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
log int | None

Override single fields of config (see :class:~tokeye.config.SpectrogramConfig). None keeps the config's value.

None
config SpectrogramConfig | Mapping[str, Any] | None

Preprocessing settings (a config or a mapping); defaults when omitted.

None
fs float | None

Default sampling rate in Hz for :meth:segment (labels axes only).

None
tile int | str | None

"auto" (default), None or an int >= 512; passed to :func:tokeye.inference.infer.

'auto'

Attributes:

Name Type Description
model_name

The model's label in results: the registry name, or a local checkpoint's file name without its directories (:func:tokeye.hub.model_label).

weights

Which weights were loaded (:func:tokeye.hub.weights_info): repo, file name, revision and sha256 for a registry model; file name and sha256 for a local checkpoint. Every :meth:segment result records it.

Source code in src/tokeye/api.py
class TokEye:
    """A loaded TokEye segmentation model plus its preprocessing settings.

    Parameters
    ----------
    model
        Registry name (downloaded and cached on first use) or a path to a
        local ``.pt``/``.pt2`` checkpoint. Instance models such as
        ``ae_tf_maskrcnn`` are rejected; use ``tokeye alfvenspec``.
    device
        ``"auto"`` (CUDA, then MPS, then CPU), ``"cpu"``, ``"cuda"``,
        ``"cuda:N"`` or ``"mps"``. An unknown or unavailable device raises
        ``ValueError``.
    n_fft, hop, clip_dc, clip_low, clip_high, log
        Override single fields of ``config`` (see
        :class:`~tokeye.config.SpectrogramConfig`). ``None`` keeps the
        config's value.
    config
        Preprocessing settings (a config or a mapping); defaults when
        omitted.
    fs
        Default sampling rate in Hz for :meth:`segment` (labels axes only).
    tile
        ``"auto"`` (default), ``None`` or an int >= 512; passed to
        :func:`tokeye.inference.infer`.

    Attributes
    ----------
    model_name
        The model's label in results: the registry name, or a local
        checkpoint's file name without its directories
        (:func:`tokeye.hub.model_label`).
    weights
        Which weights were loaded (:func:`tokeye.hub.weights_info`): repo,
        file name, revision and sha256 for a registry model; file name and
        sha256 for a local checkpoint. Every :meth:`segment` result
        records it.
    """

    def __init__(
        self,
        model: str | Path = hub.DEFAULT_MODEL,
        device: str = "auto",
        n_fft: int | None = None,
        hop: int | None = None,
        clip_dc: bool | None = None,
        clip_low: float | None = None,
        clip_high: float | None = None,
        log: bool | None = None,
        *,
        config: SpectrogramConfig | Mapping[str, Any] | None = None,
        fs: float | None = None,
        tile: int | str | None = "auto",
    ) -> None:
        overrides = {
            "n_fft": n_fft,
            "hop": hop,
            "clip_dc": clip_dc,
            "clip_low": clip_low,
            "clip_high": clip_high,
            "log": log,
        }
        cfg = SpectrogramConfig.coerce(config)
        explicit = {key: value for key, value in overrides.items() if value is not None}
        self.config = cfg.replace(**explicit) if explicit else cfg
        self.fs = _check_fs(fs)
        check_tile(tile)
        self.tile = tile

        hub.require_task(model, "segmentation")
        self.model_name = hub.model_label(model)
        self.channels = hub.channels_for(model)
        self.model = hub.load_model(model, device)
        self.weights = hub.weights_info(model)

    def __repr__(self) -> str:
        device = str(_device_of(self.model))
        return (
            f"TokEye(model={self.model_name!r}, device={device!r}, tile={self.tile!r})"
        )

    @property
    def log(self) -> bool:
        """Whether 2D inputs are ``log1p``-scaled (``config.log``)."""
        return self.config.log

    @log.setter
    def log(self, value: bool) -> None:
        self.config = self.config.replace(log=value)

    def _prepare(
        self,
        data: Any,
        *,
        fs: float | None = None,
        reference: Any = None,
        log: bool | None = None,
    ) -> Spectrogram:
        cfg = self.config if log is None else self.config.replace(log=log)
        return prepare(data, cfg, fs=self.fs if fs is None else fs, reference=reference)

    def spectrogram(self, data: Any, log: bool | None = None) -> np.ndarray:
        """The ``(H, W)`` float64 spectrogram the model will see (pre-1.0).

        1D input goes through the STFT (which log-scales internally); 2D
        input is used as-is, ``log1p``-scaled first when ``log`` is on.
        ``log=None`` defers to the instance setting. The result is always a
        new array. The model sees these values cast to float32; use
        :meth:`segment` for that float32 input together with its axes.
        """
        cfg = self.config if log is None else self.config.replace(log=log)
        values = _values64(data, cfg)[0]
        if np.shares_memory(values, data):  # 2D float64 input, log off
            values = values.copy()
        return values

    def segment(
        self,
        data: Any,
        *,
        fs: float | None = None,
        reference: Any = None,
        log: bool | None = None,
    ) -> Segmentation:
        """Segment a signal or spectrogram.

        Parameters
        ----------
        data
            1D signal or 2D spectrogram.
        fs
            Sampling rate in Hz (overrides the instance ``fs``).
        reference
            Second 1D signal for a cross-power spectrum.
        log
            Per-call override of ``config.log``.

        Returns
        -------
        Segmentation
            Mask, the spectrogram it was computed from, and its axes.
        """
        spec = self._prepare(data, fs=fs, reference=reference, log=log)
        mask = infer(self.model, spec.values, tile=self.tile)
        channels = resolve_channels(self.channels, mask.shape[0])
        return Segmentation(mask, spec, channels, self.model_name, self.weights)

    def predict(self, data: Any, log: bool | None = None) -> np.ndarray:
        """Run inference; returns a float32 mask of shape ``(C, H, W)``.

        For ``big_tf_unet``, channel 0 = coherent and channel 1 =
        transient activity, both sigmoid scores in ``[0, 1]``.
        Standardization is applied internally. A one-channel model gives
        ``(H, W)``, as before 1.0; :meth:`segment` always gives
        ``(C, H, W)``.
        """
        mask = self.segment(data, log=log).mask
        return mask[0] if mask.shape[0] == 1 else mask

    def __call__(self, data: Any, log: bool | None = None) -> np.ndarray:
        return self.predict(data, log=log)

log property writable

Whether 2D inputs are log1p-scaled (config.log).

predict(data, log=None)

Run inference; returns a float32 mask of shape (C, H, W).

For big_tf_unet, channel 0 = coherent and channel 1 = transient activity, both sigmoid scores in [0, 1]. Standardization is applied internally. A one-channel model gives (H, W), as before 1.0; :meth:segment always gives (C, H, W).

Source code in src/tokeye/api.py
def predict(self, data: Any, log: bool | None = None) -> np.ndarray:
    """Run inference; returns a float32 mask of shape ``(C, H, W)``.

    For ``big_tf_unet``, channel 0 = coherent and channel 1 =
    transient activity, both sigmoid scores in ``[0, 1]``.
    Standardization is applied internally. A one-channel model gives
    ``(H, W)``, as before 1.0; :meth:`segment` always gives
    ``(C, H, W)``.
    """
    mask = self.segment(data, log=log).mask
    return mask[0] if mask.shape[0] == 1 else mask

segment(data, *, fs=None, reference=None, log=None)

Segment a signal or spectrogram.

Parameters:

Name Type Description Default
data Any

1D signal or 2D spectrogram.

required
fs float | None

Sampling rate in Hz (overrides the instance fs).

None
reference Any

Second 1D signal for a cross-power spectrum.

None
log bool | None

Per-call override of config.log.

None

Returns:

Type Description
Segmentation

Mask, the spectrogram it was computed from, and its axes.

Source code in src/tokeye/api.py
def segment(
    self,
    data: Any,
    *,
    fs: float | None = None,
    reference: Any = None,
    log: bool | None = None,
) -> Segmentation:
    """Segment a signal or spectrogram.

    Parameters
    ----------
    data
        1D signal or 2D spectrogram.
    fs
        Sampling rate in Hz (overrides the instance ``fs``).
    reference
        Second 1D signal for a cross-power spectrum.
    log
        Per-call override of ``config.log``.

    Returns
    -------
    Segmentation
        Mask, the spectrogram it was computed from, and its axes.
    """
    spec = self._prepare(data, fs=fs, reference=reference, log=log)
    mask = infer(self.model, spec.values, tile=self.tile)
    channels = resolve_channels(self.channels, mask.shape[0])
    return Segmentation(mask, spec, channels, self.model_name, self.weights)

spectrogram(data, log=None)

The (H, W) float64 spectrogram the model will see (pre-1.0).

1D input goes through the STFT (which log-scales internally); 2D input is used as-is, log1p-scaled first when log is on. log=None defers to the instance setting. The result is always a new array. The model sees these values cast to float32; use :meth:segment for that float32 input together with its axes.

Source code in src/tokeye/api.py
def spectrogram(self, data: Any, log: bool | None = None) -> np.ndarray:
    """The ``(H, W)`` float64 spectrogram the model will see (pre-1.0).

    1D input goes through the STFT (which log-scales internally); 2D
    input is used as-is, ``log1p``-scaled first when ``log`` is on.
    ``log=None`` defers to the instance setting. The result is always a
    new array. The model sees these values cast to float32; use
    :meth:`segment` for that float32 input together with its axes.
    """
    cfg = self.config if log is None else self.config.replace(log=log)
    values = _values64(data, cfg)[0]
    if np.shares_memory(values, data):  # 2D float64 input, log off
        values = values.copy()
    return values

Preprocessing configuration: the single source of spectrogram defaults.

Standard library only, so the CLI can build its flags from :class:SpectrogramConfig without importing numpy, scipy or torch.

SpectrogramConfig dataclass

How a raw signal (or a stored spectrogram) becomes model input.

Parameters:

Name Type Description Default
n_fft int

STFT window length in samples.

1024
hop int

STFT hop in samples. 128 matches the released model's training.

128
window str

Window name understood by :func:scipy.signal.get_window.

'hann'
clip_dc bool

Drop the DC (0 Hz) row of the STFT.

True
clip_low float

Percentiles the log-magnitude spectrogram is clipped to.

1.0
clip_high float

Percentiles the log-magnitude spectrogram is clipped to.

1.0
log bool

Apply log1p to 2D inputs stored in linear scale. 1D inputs are always log-scaled by the STFT, so this only affects 2D input.

False
Notes

clip_dc and log take True or False (NumPy booleans too); the integers 0 and 1 still work, with a DeprecationWarning.

Source code in src/tokeye/config.py
@dataclass(frozen=True)
class SpectrogramConfig:
    """How a raw signal (or a stored spectrogram) becomes model input.

    Parameters
    ----------
    n_fft
        STFT window length in samples.
    hop
        STFT hop in samples. 128 matches the released model's training.
    window
        Window name understood by :func:`scipy.signal.get_window`.
    clip_dc
        Drop the DC (0 Hz) row of the STFT.
    clip_low, clip_high
        Percentiles the log-magnitude spectrogram is clipped to.
    log
        Apply ``log1p`` to 2D inputs stored in linear scale. 1D inputs
        are always log-scaled by the STFT, so this only affects 2D input.

    Notes
    -----
    ``clip_dc`` and ``log`` take ``True`` or ``False`` (NumPy booleans too);
    the integers ``0`` and ``1`` still work, with a ``DeprecationWarning``.
    """

    n_fft: int = field(default=1024, metadata={"help": "STFT window length [samples]"})
    hop: int = field(default=128, metadata={"help": "STFT hop [samples]"})
    window: str = field(default="hann", metadata={"help": "STFT window name"})
    clip_dc: bool = field(default=True, metadata={"help": "drop the DC row"})
    clip_low: float = field(default=1.0, metadata={"help": "lower clip percentile"})
    clip_high: float = field(default=99.0, metadata={"help": "upper clip percentile"})
    log: bool = field(
        default=False,
        metadata={"help": "log1p 2D inputs stored in linear scale"},
    )

    def __post_init__(self) -> None:
        for name in _INT_FIELDS:
            value = getattr(self, name)
            if isinstance(value, bool) or not isinstance(value, numbers.Integral):
                raise TypeError(f"{name} must be an integer, got {value!r}")
            object.__setattr__(self, name, int(value))
        for name in _FLOAT_FIELDS:
            value = getattr(self, name)
            if isinstance(value, bool) or not isinstance(value, numbers.Real):
                raise TypeError(f"{name} must be a number, got {value!r}")
            object.__setattr__(self, name, float(value))
        for name in _BOOL_FIELDS:
            value = getattr(self, name)
            if isinstance(value, bool):
                continue
            # Without numpy imported, the value cannot be a NumPy bool (and
            # importing it here would load numpy with this module).
            np_mod = sys.modules.get("numpy")
            if np_mod is not None and isinstance(value, np_mod.bool_):
                object.__setattr__(self, name, bool(value))
            elif isinstance(value, numbers.Integral) and value in (0, 1):
                warnings.warn(
                    f"{name}={value!r}: pass True or False (integers are "
                    "deprecated and rejected in 2.0)",
                    DeprecationWarning,
                    stacklevel=_user_stacklevel(),
                )
                object.__setattr__(self, name, bool(value))
            else:
                raise TypeError(f"{name} must be True or False, got {value!r}")
        if not isinstance(self.window, str):
            raise TypeError(f"window must be a string, got {self.window!r}")

        if not self.window:
            raise ValueError("window must be a non-empty string, got ''")
        if self.n_fft < 2:
            raise ValueError(f"n_fft must be >= 2, got {self.n_fft}")
        if self.hop < 1:
            raise ValueError(f"hop must be >= 1, got {self.hop}")
        if not 0.0 <= self.clip_low < self.clip_high <= 100.0:
            raise ValueError(
                "need 0 <= clip_low < clip_high <= 100, got "
                f"clip_low={self.clip_low}, clip_high={self.clip_high}"
            )

    def to_dict(self) -> dict[str, Any]:
        """Plain-``dict`` form (JSON-serializable)."""
        return dataclasses.asdict(self)

    @classmethod
    def from_dict(cls, data: Mapping[str, Any]) -> SpectrogramConfig:
        """Build from a mapping; unknown keys raise ``ValueError``."""
        valid = [f.name for f in dataclasses.fields(cls)]
        unknown = sorted(set(data) - set(valid), key=repr)
        if unknown:
            hint = ""
            if "fs" in unknown:
                hint = (
                    " (the sampling rate is not a preprocessing setting; "
                    "pass fs= separately)"
                )
            raise ValueError(
                f"unknown SpectrogramConfig field(s) {unknown}; valid: {valid}{hint}"
            )
        return cls(**dict(data))

    def replace(self, **changes: Any) -> SpectrogramConfig:
        """Copy with ``changes`` applied (validated like the constructor)."""
        return self.from_dict({**self.to_dict(), **changes})

    @classmethod
    def coerce(
        cls, value: SpectrogramConfig | Mapping[str, Any] | None
    ) -> SpectrogramConfig:
        """Accept ``None`` (defaults), a config, or a mapping of fields."""
        if value is None:
            return cls()
        if isinstance(value, cls):
            return value
        if isinstance(value, Mapping):
            return cls.from_dict(value)
        raise TypeError(
            f"expected SpectrogramConfig, a mapping or None, got {type(value).__name__}"
        )

    def stft_kwargs(self) -> dict[str, Any]:
        """Keyword arguments for :func:`tokeye.transforms.compute_stft`."""
        return {
            "n_fft": self.n_fft,
            "hop": self.hop,
            "window": self.window,
            "clip_dc": self.clip_dc,
            "clip_low": self.clip_low,
            "clip_high": self.clip_high,
        }

coerce(value) classmethod

Accept None (defaults), a config, or a mapping of fields.

Source code in src/tokeye/config.py
@classmethod
def coerce(
    cls, value: SpectrogramConfig | Mapping[str, Any] | None
) -> SpectrogramConfig:
    """Accept ``None`` (defaults), a config, or a mapping of fields."""
    if value is None:
        return cls()
    if isinstance(value, cls):
        return value
    if isinstance(value, Mapping):
        return cls.from_dict(value)
    raise TypeError(
        f"expected SpectrogramConfig, a mapping or None, got {type(value).__name__}"
    )

from_dict(data) classmethod

Build from a mapping; unknown keys raise ValueError.

Source code in src/tokeye/config.py
@classmethod
def from_dict(cls, data: Mapping[str, Any]) -> SpectrogramConfig:
    """Build from a mapping; unknown keys raise ``ValueError``."""
    valid = [f.name for f in dataclasses.fields(cls)]
    unknown = sorted(set(data) - set(valid), key=repr)
    if unknown:
        hint = ""
        if "fs" in unknown:
            hint = (
                " (the sampling rate is not a preprocessing setting; "
                "pass fs= separately)"
            )
        raise ValueError(
            f"unknown SpectrogramConfig field(s) {unknown}; valid: {valid}{hint}"
        )
    return cls(**dict(data))

replace(**changes)

Copy with changes applied (validated like the constructor).

Source code in src/tokeye/config.py
def replace(self, **changes: Any) -> SpectrogramConfig:
    """Copy with ``changes`` applied (validated like the constructor)."""
    return self.from_dict({**self.to_dict(), **changes})

stft_kwargs()

Keyword arguments for :func:tokeye.transforms.compute_stft.

Source code in src/tokeye/config.py
def stft_kwargs(self) -> dict[str, Any]:
    """Keyword arguments for :func:`tokeye.transforms.compute_stft`."""
    return {
        "n_fft": self.n_fft,
        "hop": self.hop,
        "window": self.window,
        "clip_dc": self.clip_dc,
        "clip_low": self.clip_low,
        "clip_high": self.clip_high,
    }

to_dict()

Plain-dict form (JSON-serializable).

Source code in src/tokeye/config.py
def to_dict(self) -> dict[str, Any]:
    """Plain-``dict`` form (JSON-serializable)."""
    return dataclasses.asdict(self)

resolve_channels(preferred, n)

Channel names for an n-channel mask.

Returns preferred when it has exactly n names, otherwise the generic ("channel_0", "channel_1", ...).

Source code in src/tokeye/config.py
def resolve_channels(preferred: tuple[str, ...], n: int) -> tuple[str, ...]:
    """Channel names for an ``n``-channel mask.

    Returns ``preferred`` when it has exactly ``n`` names, otherwise the
    generic ``("channel_0", "channel_1", ...)``.
    """
    if len(preferred) == n:
        return tuple(preferred)
    return tuple(f"channel_{i}" for i in range(n))

Segmentation: a TokEye mask together with the spectrogram it came from.

Segmentation dataclass

The result of :meth:tokeye.TokEye.segment.

Attributes:

Name Type Description
mask ndarray

(C, H, W) float32 scores in [0, 1]; for big_tf_unet, channel 0 is coherent and channel 1 transient activity.

spectrogram Spectrogram

The :class:~tokeye.preprocess.Spectrogram the model saw.

channels tuple[str, ...]

One name per mask channel.

model str

Registry name or file name of the model that produced the mask.

weights Mapping[str, str | None] | None

Which weights produced it (see :func:tokeye.hub.weights_info): repo, filename, revision and sha256 for a registry model, name and sha256 for a local checkpoint; None when unknown. A mapping is stored as a plain dict.

Source code in src/tokeye/result.py
@dataclass(frozen=True, eq=False)
class Segmentation:
    """The result of :meth:`tokeye.TokEye.segment`.

    Attributes
    ----------
    mask
        ``(C, H, W)`` float32 scores in ``[0, 1]``; for ``big_tf_unet``,
        channel 0 is coherent and channel 1 transient activity.
    spectrogram
        The :class:`~tokeye.preprocess.Spectrogram` the model saw.
    channels
        One name per mask channel.
    model
        Registry name or file name of the model that produced the mask.
    weights
        Which weights produced it (see :func:`tokeye.hub.weights_info`):
        ``repo``, ``filename``, ``revision`` and ``sha256`` for a registry
        model, ``name`` and ``sha256`` for a local checkpoint; ``None`` when
        unknown. A mapping is stored as a plain ``dict``.
    """

    mask: np.ndarray
    spectrogram: Spectrogram
    channels: tuple[str, ...] = DEFAULT_CHANNELS
    model: str = "big_tf_unet"
    weights: Mapping[str, str | None] | None = None

    def __post_init__(self) -> None:
        if not isinstance(self.spectrogram, Spectrogram):
            raise TypeError(
                "spectrogram must be a tokeye Spectrogram, got "
                f"{type(self.spectrogram).__name__}"
            )
        if self.weights is not None:
            if not isinstance(self.weights, Mapping):
                raise TypeError(
                    "weights must be a mapping or None, got "
                    f"{type(self.weights).__name__}"
                )
            object.__setattr__(self, "weights", dict(self.weights))
        mask = np.asarray(self.mask, dtype=np.float32)
        if mask.ndim != 3 or mask.shape[1:] != self.spectrogram.shape:
            raise ValueError(
                f"mask shape {mask.shape} does not match spectrogram shape "
                f"{self.spectrogram.shape}; expected (C, H, W)"
            )
        channels = tuple(self.channels)
        if len(channels) != mask.shape[0]:
            raise ValueError(
                f"{len(channels)} channel name(s) for a {mask.shape[0]}-channel mask"
            )
        object.__setattr__(self, "mask", mask)
        object.__setattr__(self, "channels", channels)

    def __repr__(self) -> str:
        return (
            f"Segmentation(model={self.model!r}, mask={self.mask.shape}, "
            f"channels={self.channels!r}, "
            f"{_axes_text(self.fs, self.freqs, self.times)})"
        )

    def __getitem__(self, name: str) -> np.ndarray:
        """The ``(H, W)`` scores of channel ``name``."""
        try:
            return self.mask[self.channels.index(name)]
        except ValueError:
            raise KeyError(
                f"no channel {name!r}; channels: {list(self.channels)}"
            ) from None

    def _channel_attribute(self, name: str) -> np.ndarray:
        # AttributeError, so hasattr()/getattr(..., default) work when the
        # channel is absent; seg[name] keeps raising KeyError.
        try:
            return self[name]
        except KeyError as exc:
            raise AttributeError(exc.args[0]) from None

    @property
    def coherent(self) -> np.ndarray:
        """``(H, W)`` coherent-activity scores (``AttributeError`` if absent)."""
        return self._channel_attribute("coherent")

    @property
    def transient(self) -> np.ndarray:
        """``(H, W)`` transient-activity scores (``AttributeError`` if absent)."""
        return self._channel_attribute("transient")

    @property
    def freqs(self) -> np.ndarray:
        """Row centres: Hz when ``fs`` is known, else bin index."""
        return self.spectrogram.freqs

    @property
    def times(self) -> np.ndarray:
        """Column centres: seconds when ``fs`` is known, else frame index."""
        return self.spectrogram.times

    @property
    def fs(self) -> float | None:
        """Sampling rate in Hz, or ``None``."""
        return self.spectrogram.fs

    def threshold(self, t: float = 0.5) -> np.ndarray:
        """Boolean ``(C, H, W)`` mask, ``True`` where the score is ``>= t``."""
        return self.mask >= t

    def save(self, path: str | Path) -> Path:
        """Save as a ``tokeye-analysis/v1`` ``.npz`` (the app's schema).

        Returns the path written (``.npz`` is appended when missing). With
        ``fs`` known, the result's own axes are stored (in ms and kHz).
        Without it the axes are bin and frame indices and are not stored;
        other axes then warn (``UserWarning``), since they reload as indices.
        """
        spec = self.spectrogram
        axes = None
        if spec.fs is not None:
            axes = (
                np.asarray(spec.times, dtype=np.float64) * 1e3,
                np.asarray(spec.freqs, dtype=np.float64) / 1e3,
            )
        params = {
            "config": spec.config.to_dict(),
            "model": self.model,
            "weights": self.weights,
            "tokeye_version": __version__,
            "kind": spec.kind,
            "fs": spec.fs,
            "n_samples": None if spec.n_samples is None else int(spec.n_samples),
            "channels": list(self.channels),
        }
        bundle = export.analysis_bundle(
            spectrogram=spec.values,
            mask=self.mask,
            axes=axes,
            params=params,
            source="segment",
        )
        written = export.save_npz(path, bundle)
        if spec.fs is None:
            freqs, times = _axes(spec.shape, spec.config, None)
            if not (
                np.array_equal(spec.freqs, freqs) and np.array_equal(spec.times, times)
            ):
                warnings.warn(
                    f"{written}: axes without fs are not saved; they reload as "
                    "bin/frame indices",
                    UserWarning,
                    stacklevel=_user_stacklevel(),
                )
        return written

    @classmethod
    def load(cls, path: str | Path) -> Segmentation:
        """Load a ``tokeye-analysis/v1`` bundle that contains a mask.

        Bundles written by the app (flat ``n_fft``/``hop``/... params, no
        axes) load too; missing settings fall back to the defaults, silently.
        A recorded value that is invalid (an unknown or out-of-range setting,
        a bad ``fs``, ``kind``, ``n_samples``, channel list, weights record or
        stored axis) is not used: it falls back to its default with a
        ``UserWarning`` naming the file. Bundles without a weights record
        load with ``weights=None``, and without ``n_samples`` with
        ``n_samples=None``.

        Bundles do not record their STFT framing, so their stored axes
        (``time_ms``/``freq_khz``) are the only record of it: with a valid
        ``fs``, each valid stored axis is used as stored, and only a missing
        or invalid one is rebuilt from ``fs`` and the settings.

        Raises
        ------
        ValueError
            The file is not a ``tokeye-analysis/v1`` bundle (a bare ``.npy``
            mask, say: ``tokeye run --format npz`` and :meth:`save` write
            bundles), or has no mask.
        """
        loaded = np.load(Path(path), allow_pickle=False)
        if isinstance(loaded, np.ndarray):
            raise ValueError(
                f"{path} is a bare array (such as the mask `tokeye run` writes by "
                f"default), not a {export.SCHEMA_ANALYSIS} bundle; "
                "`tokeye run --format npz` or Segmentation.save() writes a bundle "
                "that loads"
            )
        with loaded as data:
            files = set(data.files)
            schema = str(data["schema"]) if "schema" in files else None
            if schema != export.SCHEMA_ANALYSIS:
                raise ValueError(
                    f"{path} is not a {export.SCHEMA_ANALYSIS} bundle "
                    f"(schema={schema!r})"
                )
            if "mask" not in files:
                raise ValueError(f"{path} has no mask (saved before inference?)")
            values = np.asarray(data["spectrogram"], dtype=np.float32)
            mask = np.asarray(data["mask"], dtype=np.float32)
            params_json = str(data["params_json"]) if "params_json" in files else "{}"
            time_ms = data["time_ms"] if "time_ms" in files else None
            freq_khz = data["freq_khz"] if "freq_khz" in files else None

        if mask.ndim == 2:
            mask = mask[np.newaxis]
        params = _params_from_json(path, params_json)
        cfg = _config_from_params(params, path)

        kind = params.get("kind")
        if kind is None:
            kind = "spectrogram"
        elif kind not in KINDS:
            _warn_ignored(path, "kind", kind, f"not one of {KINDS}", "'spectrogram'")
            kind = "spectrogram"

        fs = params.get("fs")
        if fs is not None and not _positive_finite(fs):
            _warn_ignored(
                path,
                "fs",
                fs,
                "not a positive finite number",
                "no fs: the axes are rebuilt as bin/frame indices",
            )
            fs = None
        fs = None if fs is None else float(fs)

        freqs, times = _axes(values.shape, cfg, fs)
        if fs is not None:
            stored_times = _stored_axis(path, "time_ms", time_ms, values.shape[1])
            if stored_times is not None:
                times = stored_times / 1e3
            stored_freqs = _stored_axis(path, "freq_khz", freq_khz, values.shape[0])
            if stored_freqs is not None:
                freqs = stored_freqs * 1e3
        n_samples = _n_samples_from(path, params.get("n_samples"))
        spectrogram = Spectrogram(values, freqs, times, kind, cfg, fs, n_samples)

        default_channels = resolve_channels(DEFAULT_CHANNELS, mask.shape[0])
        channels = params.get("channels")
        if channels is None:
            channels = default_channels
        elif not (
            isinstance(channels, list)
            and len(channels) == mask.shape[0]
            and all(isinstance(name, str) for name in channels)
        ):
            _warn_ignored(
                path,
                "channels",
                channels,
                f"not a list of {mask.shape[0]} names",
                f"{list(default_channels)}",
            )
            channels = default_channels
        model = params.get("model") or "unknown"
        weights = params.get("weights")
        if weights is not None and not _is_weights_record(weights):
            _warn_ignored(
                path,
                "weights",
                weights,
                "not a mapping of strings to strings or null",
                "None",
            )
            weights = None
        return cls(mask, spectrogram, tuple(channels), str(model), weights)

    def plot(
        self, ax: Axes | None = None, *, threshold: float = 0.5, alpha: float = 0.4
    ) -> Axes:
        """Draw the spectrogram with the thresholded mask overlaid.

        Uses ``origin="lower"`` (low frequencies at the bottom) and labelled
        axes. Creates a new pyplot figure when ``ax`` is ``None``.
        """
        from ._plotting import draw_segmentation

        if ax is None:
            import matplotlib.pyplot as plt

            _, ax = plt.subplots(figsize=(8, 4), layout="constrained")
        return draw_segmentation(ax, self, threshold=threshold, alpha=alpha)

coherent property

(H, W) coherent-activity scores (AttributeError if absent).

freqs property

Row centres: Hz when fs is known, else bin index.

fs property

Sampling rate in Hz, or None.

times property

Column centres: seconds when fs is known, else frame index.

transient property

(H, W) transient-activity scores (AttributeError if absent).

__getitem__(name)

The (H, W) scores of channel name.

Source code in src/tokeye/result.py
def __getitem__(self, name: str) -> np.ndarray:
    """The ``(H, W)`` scores of channel ``name``."""
    try:
        return self.mask[self.channels.index(name)]
    except ValueError:
        raise KeyError(
            f"no channel {name!r}; channels: {list(self.channels)}"
        ) from None

load(path) classmethod

Load a tokeye-analysis/v1 bundle that contains a mask.

Bundles written by the app (flat n_fft/hop/... params, no axes) load too; missing settings fall back to the defaults, silently. A recorded value that is invalid (an unknown or out-of-range setting, a bad fs, kind, n_samples, channel list, weights record or stored axis) is not used: it falls back to its default with a UserWarning naming the file. Bundles without a weights record load with weights=None, and without n_samples with n_samples=None.

Bundles do not record their STFT framing, so their stored axes (time_ms/freq_khz) are the only record of it: with a valid fs, each valid stored axis is used as stored, and only a missing or invalid one is rebuilt from fs and the settings.

Raises:

Type Description
ValueError

The file is not a tokeye-analysis/v1 bundle (a bare .npy mask, say: tokeye run --format npz and :meth:save write bundles), or has no mask.

Source code in src/tokeye/result.py
@classmethod
def load(cls, path: str | Path) -> Segmentation:
    """Load a ``tokeye-analysis/v1`` bundle that contains a mask.

    Bundles written by the app (flat ``n_fft``/``hop``/... params, no
    axes) load too; missing settings fall back to the defaults, silently.
    A recorded value that is invalid (an unknown or out-of-range setting,
    a bad ``fs``, ``kind``, ``n_samples``, channel list, weights record or
    stored axis) is not used: it falls back to its default with a
    ``UserWarning`` naming the file. Bundles without a weights record
    load with ``weights=None``, and without ``n_samples`` with
    ``n_samples=None``.

    Bundles do not record their STFT framing, so their stored axes
    (``time_ms``/``freq_khz``) are the only record of it: with a valid
    ``fs``, each valid stored axis is used as stored, and only a missing
    or invalid one is rebuilt from ``fs`` and the settings.

    Raises
    ------
    ValueError
        The file is not a ``tokeye-analysis/v1`` bundle (a bare ``.npy``
        mask, say: ``tokeye run --format npz`` and :meth:`save` write
        bundles), or has no mask.
    """
    loaded = np.load(Path(path), allow_pickle=False)
    if isinstance(loaded, np.ndarray):
        raise ValueError(
            f"{path} is a bare array (such as the mask `tokeye run` writes by "
            f"default), not a {export.SCHEMA_ANALYSIS} bundle; "
            "`tokeye run --format npz` or Segmentation.save() writes a bundle "
            "that loads"
        )
    with loaded as data:
        files = set(data.files)
        schema = str(data["schema"]) if "schema" in files else None
        if schema != export.SCHEMA_ANALYSIS:
            raise ValueError(
                f"{path} is not a {export.SCHEMA_ANALYSIS} bundle "
                f"(schema={schema!r})"
            )
        if "mask" not in files:
            raise ValueError(f"{path} has no mask (saved before inference?)")
        values = np.asarray(data["spectrogram"], dtype=np.float32)
        mask = np.asarray(data["mask"], dtype=np.float32)
        params_json = str(data["params_json"]) if "params_json" in files else "{}"
        time_ms = data["time_ms"] if "time_ms" in files else None
        freq_khz = data["freq_khz"] if "freq_khz" in files else None

    if mask.ndim == 2:
        mask = mask[np.newaxis]
    params = _params_from_json(path, params_json)
    cfg = _config_from_params(params, path)

    kind = params.get("kind")
    if kind is None:
        kind = "spectrogram"
    elif kind not in KINDS:
        _warn_ignored(path, "kind", kind, f"not one of {KINDS}", "'spectrogram'")
        kind = "spectrogram"

    fs = params.get("fs")
    if fs is not None and not _positive_finite(fs):
        _warn_ignored(
            path,
            "fs",
            fs,
            "not a positive finite number",
            "no fs: the axes are rebuilt as bin/frame indices",
        )
        fs = None
    fs = None if fs is None else float(fs)

    freqs, times = _axes(values.shape, cfg, fs)
    if fs is not None:
        stored_times = _stored_axis(path, "time_ms", time_ms, values.shape[1])
        if stored_times is not None:
            times = stored_times / 1e3
        stored_freqs = _stored_axis(path, "freq_khz", freq_khz, values.shape[0])
        if stored_freqs is not None:
            freqs = stored_freqs * 1e3
    n_samples = _n_samples_from(path, params.get("n_samples"))
    spectrogram = Spectrogram(values, freqs, times, kind, cfg, fs, n_samples)

    default_channels = resolve_channels(DEFAULT_CHANNELS, mask.shape[0])
    channels = params.get("channels")
    if channels is None:
        channels = default_channels
    elif not (
        isinstance(channels, list)
        and len(channels) == mask.shape[0]
        and all(isinstance(name, str) for name in channels)
    ):
        _warn_ignored(
            path,
            "channels",
            channels,
            f"not a list of {mask.shape[0]} names",
            f"{list(default_channels)}",
        )
        channels = default_channels
    model = params.get("model") or "unknown"
    weights = params.get("weights")
    if weights is not None and not _is_weights_record(weights):
        _warn_ignored(
            path,
            "weights",
            weights,
            "not a mapping of strings to strings or null",
            "None",
        )
        weights = None
    return cls(mask, spectrogram, tuple(channels), str(model), weights)

plot(ax=None, *, threshold=0.5, alpha=0.4)

Draw the spectrogram with the thresholded mask overlaid.

Uses origin="lower" (low frequencies at the bottom) and labelled axes. Creates a new pyplot figure when ax is None.

Source code in src/tokeye/result.py
def plot(
    self, ax: Axes | None = None, *, threshold: float = 0.5, alpha: float = 0.4
) -> Axes:
    """Draw the spectrogram with the thresholded mask overlaid.

    Uses ``origin="lower"`` (low frequencies at the bottom) and labelled
    axes. Creates a new pyplot figure when ``ax`` is ``None``.
    """
    from ._plotting import draw_segmentation

    if ax is None:
        import matplotlib.pyplot as plt

        _, ax = plt.subplots(figsize=(8, 4), layout="constrained")
    return draw_segmentation(ax, self, threshold=threshold, alpha=alpha)

save(path)

Save as a tokeye-analysis/v1 .npz (the app's schema).

Returns the path written (.npz is appended when missing). With fs known, the result's own axes are stored (in ms and kHz). Without it the axes are bin and frame indices and are not stored; other axes then warn (UserWarning), since they reload as indices.

Source code in src/tokeye/result.py
def save(self, path: str | Path) -> Path:
    """Save as a ``tokeye-analysis/v1`` ``.npz`` (the app's schema).

    Returns the path written (``.npz`` is appended when missing). With
    ``fs`` known, the result's own axes are stored (in ms and kHz).
    Without it the axes are bin and frame indices and are not stored;
    other axes then warn (``UserWarning``), since they reload as indices.
    """
    spec = self.spectrogram
    axes = None
    if spec.fs is not None:
        axes = (
            np.asarray(spec.times, dtype=np.float64) * 1e3,
            np.asarray(spec.freqs, dtype=np.float64) / 1e3,
        )
    params = {
        "config": spec.config.to_dict(),
        "model": self.model,
        "weights": self.weights,
        "tokeye_version": __version__,
        "kind": spec.kind,
        "fs": spec.fs,
        "n_samples": None if spec.n_samples is None else int(spec.n_samples),
        "channels": list(self.channels),
    }
    bundle = export.analysis_bundle(
        spectrogram=spec.values,
        mask=self.mask,
        axes=axes,
        params=params,
        source="segment",
    )
    written = export.save_npz(path, bundle)
    if spec.fs is None:
        freqs, times = _axes(spec.shape, spec.config, None)
        if not (
            np.array_equal(spec.freqs, freqs) and np.array_equal(spec.times, times)
        ):
            warnings.warn(
                f"{written}: axes without fs are not saved; they reload as "
                "bin/frame indices",
                UserWarning,
                stacklevel=_user_stacklevel(),
            )
    return written

threshold(t=0.5)

Boolean (C, H, W) mask, True where the score is >= t.

Source code in src/tokeye/result.py
def threshold(self, t: float = 0.5) -> np.ndarray:
    """Boolean ``(C, H, W)`` mask, ``True`` where the score is ``>= t``."""
    return self.mask >= t

Turn user data into the spectrogram the model sees.

:func:prepare is the one preprocessing entry point shared by the Python API, the CLI, batch runs and the app:

  • a 1D signal becomes a log-magnitude STFT (kind="stft");
  • a 1D signal plus reference= becomes a cross-power STFT (kind="cross");
  • a 2D array is taken as a ready spectrogram (kind="spectrogram"), log1p-scaled first when config.log is on.

The STFT frames are centred as in the model's training (see :func:tokeye.transforms.compute_stft), so N samples give 1 + N // hop columns for an even n_fft. Every kind has the same axes: row r is FFT bin b = r + 1 with clip_dc (else b = r), at b * fs / n_fft Hz, and column j is at j * hop / fs seconds from the first sample. Without fs they are the bin and frame indices.

Spectrogram dataclass

A model-ready spectrogram and its axes.

Attributes:

Name Type Description
values ndarray

(H, W) float32 image, rows = frequency (row 0 lowest).

freqs ndarray

(H,) row centres: FFT bin b (r + 1 with clip_dc, else r) at b * fs / n_fft Hz when fs is known, else the bin index b.

times ndarray

(W,) column centres: j * hop / fs seconds from the first sample when fs is known, else the frame index j.

kind str

"stft", "cross" or "spectrogram".

config SpectrogramConfig

The :class:~tokeye.config.SpectrogramConfig used.

fs float | None

Sampling rate in Hz, or None when unknown.

n_samples int | None

Length of the 1D signal (and of its reference) the values came from, or None for 2D input. The signal lasts n_samples / fs seconds; the columns span only n_samples // hop hops.

Source code in src/tokeye/preprocess.py
@dataclass(frozen=True, eq=False)
class Spectrogram:
    """A model-ready spectrogram and its axes.

    Attributes
    ----------
    values
        ``(H, W)`` float32 image, rows = frequency (row 0 lowest).
    freqs
        ``(H,)`` row centres: FFT bin ``b`` (``r + 1`` with ``clip_dc``,
        else ``r``) at ``b * fs / n_fft`` Hz when ``fs`` is known, else the
        bin index ``b``.
    times
        ``(W,)`` column centres: ``j * hop / fs`` seconds from the first
        sample when ``fs`` is known, else the frame index ``j``.
    kind
        ``"stft"``, ``"cross"`` or ``"spectrogram"``.
    config
        The :class:`~tokeye.config.SpectrogramConfig` used.
    fs
        Sampling rate in Hz, or ``None`` when unknown.
    n_samples
        Length of the 1D signal (and of its reference) the values came
        from, or ``None`` for 2D input. The signal lasts ``n_samples / fs``
        seconds; the columns span only ``n_samples // hop`` hops.
    """

    values: np.ndarray
    freqs: np.ndarray
    times: np.ndarray
    kind: str
    config: SpectrogramConfig
    fs: float | None
    n_samples: int | None = None

    @property
    def shape(self) -> tuple[int, int]:
        return self.values.shape

    def __repr__(self) -> str:
        return (
            f"Spectrogram(kind={self.kind!r}, shape={np.shape(self.values)}, "
            f"{_axes_text(self.fs, self.freqs, self.times)})"
        )

prepare(data, config=None, *, fs=None, reference=None)

Build the :class:Spectrogram the model will see.

Parameters:

Name Type Description Default
data Any

A 1D signal or a 2D (freq, time) spectrogram (array-like, real). A 1D signal becomes centred STFT frames (see :func:tokeye.transforms.compute_stft): 1 + N // hop columns for N samples and an even n_fft.

required
config SpectrogramConfig | Mapping[str, Any] | None

Preprocessing settings; None uses the defaults.

None
fs float | None

Sampling rate in Hz. Only labels the axes; it never changes values: row r is FFT bin b (r + 1 with clip_dc) at b * fs / n_fft Hz, and column j is at j * hop / fs seconds. For 2D input the axes assume the config's n_fft, hop and clip_dc, and centred frames.

None
reference Any

A second 1D signal, same length as data, for a cross-power spectrum.

None

Raises:

Type Description
TypeError

fs is not a number (a bool, string or bytes value included).

ValueError

On complex, non-numeric, empty or non-finite input, a bad shape (including a 2D input with a single row or column, which is a 1D signal stored as a vector), a signal shorter than n_fft // 2 + 1 samples, a bad fs, or a mismatched reference.

Source code in src/tokeye/preprocess.py
def prepare(
    data: Any,
    config: SpectrogramConfig | Mapping[str, Any] | None = None,
    *,
    fs: float | None = None,
    reference: Any = None,
) -> Spectrogram:
    """Build the :class:`Spectrogram` the model will see.

    Parameters
    ----------
    data
        A 1D signal or a 2D ``(freq, time)`` spectrogram (array-like, real).
        A 1D signal becomes centred STFT frames (see
        :func:`tokeye.transforms.compute_stft`): ``1 + N // hop`` columns
        for ``N`` samples and an even ``n_fft``.
    config
        Preprocessing settings; ``None`` uses the defaults.
    fs
        Sampling rate in Hz. Only labels the axes; it never changes values:
        row ``r`` is FFT bin ``b`` (``r + 1`` with ``clip_dc``) at
        ``b * fs / n_fft`` Hz, and column ``j`` is at ``j * hop / fs``
        seconds. For 2D input the axes assume the config's ``n_fft``,
        ``hop`` and ``clip_dc``, and centred frames.
    reference
        A second 1D signal, same length as ``data``, for a cross-power
        spectrum.

    Raises
    ------
    TypeError
        ``fs`` is not a number (a bool, string or bytes value included).
    ValueError
        On complex, non-numeric, empty or non-finite input, a bad shape
        (including a 2D input with a single row or column, which is a 1D
        signal stored as a vector), a signal shorter than
        ``n_fft // 2 + 1`` samples, a bad ``fs``, or a mismatched
        ``reference``.
    """
    cfg = SpectrogramConfig.coerce(config)
    fs = _check_fs(fs)
    values64, kind, n_samples = _values64(data, cfg, reference)
    if kind == "spectrogram" and 1 in values64.shape:
        raise ValueError(
            f"data has shape {values64.shape}; for a 1D signal pass np.ravel(data) "
            "(a spectrogram needs at least 2 rows and 2 columns)"
        )
    # Always C-ordered (a plain astype keeps an F-ordered input's layout).
    values = np.ascontiguousarray(values64, dtype=np.float32)
    freqs, times = _axes(values.shape, cfg, fs)
    return Spectrogram(values, freqs, times, kind, cfg, fs, n_samples)

Model inference: standardize, tile when large, forward, sigmoid.

:func:infer is the 1.0 entry point. :func:model_infer, :func:signal_to_spectrogram and :func:warmup keep their pre-1.0 behaviour for existing callers (the app, dev scripts).

check_tile(tile)

Raise if tile is not "auto", None or an int >= 512.

Source code in src/tokeye/inference.py
def check_tile(tile: int | str | None) -> None:
    """Raise if ``tile`` is not ``"auto"``, ``None`` or an int >= 512."""
    if tile is None or tile == "auto":
        return
    if isinstance(tile, str):
        raise ValueError(f"tile must be 'auto', None or an int, got {tile!r}")
    if isinstance(tile, bool) or not isinstance(tile, numbers.Integral):
        raise TypeError(f"tile must be 'auto', None or an int, got {tile!r}")
    if tile < MIN_TILE:
        raise ValueError(f"tile must be >= {MIN_TILE}, got {tile}")

infer(model, values, *, device=None, tile='auto')

Segment one spectrogram.

Parameters:

Name Type Description Default
model Module

A segmentation model mapping (1, 1, H, W) to (1, C, H, W) logits (a tuple/list whose first item is that tensor also works).

required
values ndarray

(H, W) spectrogram, log-scaled as the model expects (see :func:tokeye.preprocess.prepare). Both sides must be >= 16.

required
device str | device | None

Where to run. None uses the device the model is on; otherwise the model is moved there. If an op is unsupported on MPS, the model moves to the CPU with one RuntimeWarning.

None
tile int | str | None

"auto" (default) runs untiled up to 2**21 pixels and tiles larger inputs; an int >= 512 sets the tile side; None never tiles. Standardization always uses the whole image. Tiled output matches an untiled run to float32 rounding for big_tf_unet and any BigTFUNetModel of the default depth or shallower (num_layers <= 5). Other models still tile, and TokEye warns (one UserWarning) when it can tell that tiles may differ: TorchScript and torch.fx (.pt2) models, other align_corners=True upsampling, deeper models, feature maps that do not halve by flooring, and models copy.deepcopy cannot copy. It cannot tell for a model that upsamples through F.interpolate inside forward, or whose receptive field is wider than the 128-px margin.

'auto'

Returns:

Type Description
ndarray

(C, H, W) float32 sigmoid scores in [0, 1].

Source code in src/tokeye/inference.py
def infer(
    model: nn.Module,
    values: np.ndarray,
    *,
    device: str | torch.device | None = None,
    tile: int | str | None = "auto",
) -> np.ndarray:
    """Segment one spectrogram.

    Parameters
    ----------
    model
        A segmentation model mapping ``(1, 1, H, W)`` to ``(1, C, H, W)``
        logits (a tuple/list whose first item is that tensor also works).
    values
        ``(H, W)`` spectrogram, log-scaled as the model expects (see
        :func:`tokeye.preprocess.prepare`). Both sides must be >= 16.
    device
        Where to run. ``None`` uses the device the model is on; otherwise
        the model is moved there. If an op is unsupported on MPS, the
        model moves to the CPU with one ``RuntimeWarning``.
    tile
        ``"auto"`` (default) runs untiled up to ``2**21`` pixels and tiles
        larger inputs; an int ``>= 512`` sets the tile side; ``None``
        never tiles. Standardization always uses the whole image. Tiled
        output matches an untiled run to float32 rounding for
        ``big_tf_unet`` and any ``BigTFUNetModel`` of the default depth or
        shallower (``num_layers <= 5``). Other models still tile, and
        TokEye warns (one ``UserWarning``) when it can tell that tiles may
        differ: TorchScript and ``torch.fx`` (``.pt2``) models, other
        ``align_corners=True`` upsampling, deeper models, feature maps that
        do not halve by flooring, and models ``copy.deepcopy`` cannot copy.
        It cannot tell for a model that upsamples through ``F.interpolate``
        inside ``forward``, or whose receptive field is wider than the
        128-px margin.

    Returns
    -------
    numpy.ndarray
        ``(C, H, W)`` float32 sigmoid scores in ``[0, 1]``.
    """
    arr = _check_values(values)
    dev = _device_of(model) if device is None else torch.device(device)
    plan = _plan_tiles(arr.shape, tile)

    def run(target: torch.device) -> tuple[np.ndarray, bool]:
        if device is not None:
            model.to(target)
        return _run(model, arr, plan, target)

    out, exact = _with_cpu_fallback(model, dev, run)
    if not exact:
        warnings.warn(TILE_WARNING, UserWarning, stacklevel=2)
    return out

model_infer(inp_array, model)

Pre-1.0 inference helper: one untiled pass on the model's device.

Unlike :func:infer, it does no input validation (shape, dtype, minimum size or finiteness). None for either argument passes through: it returns None and logs a warning. It has the same MPS->CPU fallback as :func:infer.

Returns:

Type Description
ndarray or None

(C, H, W) sigmoid scores, or (H, W) for a one-channel model.

Source code in src/tokeye/inference.py
def model_infer(
    inp_array: np.ndarray | None,
    model: nn.Module | None,
) -> np.ndarray | None:
    """Pre-1.0 inference helper: one untiled pass on the model's device.

    Unlike :func:`infer`, it does no input validation (shape, dtype,
    minimum size or finiteness). ``None`` for either argument passes
    through: it returns ``None`` and logs a warning. It has the same
    MPS->CPU fallback as :func:`infer`.

    Returns
    -------
    numpy.ndarray or None
        ``(C, H, W)`` sigmoid scores, or ``(H, W)`` for a one-channel model.
    """
    if inp_array is None or model is None:
        logger.warning("Missing input or model for inference")
        return None

    arr = np.asarray(inp_array)
    logger.info("Running inference on input shape: %s", arr.shape)
    out, _ = _with_cpu_fallback(
        model, _device_of(model), lambda target: _run(model, arr, None, target)
    )
    return out[0] if out.shape[0] == 1 else out

signal_to_spectrogram(signal, **stft_kwargs)

Expand a 1D signal to (1, N) and run it through compute_stft.

Source code in src/tokeye/inference.py
def signal_to_spectrogram(signal: np.ndarray, **stft_kwargs) -> np.ndarray:
    """Expand a 1D signal to (1, N) and run it through ``compute_stft``."""
    signal_data = np.expand_dims(signal, axis=0)
    return compute_stft(signal_data, **stft_kwargs)

warmup(model, iterations=10)

Run dummy forward passes to trigger lazy init / kernel autotuning.

Source code in src/tokeye/inference.py
def warmup(model: nn.Module, iterations: int = 10) -> None:
    """Run dummy forward passes to trigger lazy init / kernel autotuning."""
    device = _device_of(model)
    dummy_input = torch.randn(*WARMUP_INPUT_SHAPE, device=device, dtype=torch.float32)
    with torch.inference_mode():
        for _ in tqdm(range(iterations), disable=None):  # no bar off a terminal
            _ = model(dummy_input)

Read signals and spectrograms from common file formats.

:func:load_signal returns (array, fs): the array is a 1D signal or a 2D spectrogram, fs the sampling rate in Hz when the file says so (WAV, FLAC and OGG headers; an fs-like field in .npz/.mat/.h5; a time column in .csv/.txt; or an _sr<fs> filename suffix such as shot_sr500000.npy), else None.

A container (.npz, .mat, .h5/.hdf5) can hold several arrays. TokEye reads the one named data, signal, x, spectrogram, values or y, else the only numeric one; when several qualify it raises and lists them, and key= picks one.

fs_from_name(path)

Sampling rate from an _sr<fs> stem suffix (x_sr200000.npy).

Source code in src/tokeye/io.py
def fs_from_name(path: str | Path) -> float | None:
    """Sampling rate from an ``_sr<fs>`` stem suffix (``x_sr200000.npy``)."""
    match = _SR_SUFFIX.search(Path(path).stem)
    if match is None:
        return None
    return _positive(float(match.group(1)))

load_signal(path, key=None)

Load a 1D signal or 2D spectrogram and its sampling rate.

Parameters:

Name Type Description Default
path str | Path

A file with one of the suffixes in :data:SIGNAL_SUFFIXES.

required
key str | None

Which array to read from a container (.npz, .mat, .h5, .hdf5): an entry name, or an HDF5 dataset path (/ first for an exact path from the root) or its trailing whole path components, such as tree/pointname/data. By default, the one array named data, signal, x, spectrogram, values or y, else the only numeric array with more than one element.

None

Returns:

Type Description
tuple

(array, fs); (1, N) and (N, 1) arrays are flattened to 1D, and fs is None when the file does not record it. A 2-column .csv/.txt is read as (time, value), with time in seconds unless the header names the column in ms or us.

Raises:

Type Description
FileNotFoundError

If path is not a file.

TypeError

key is not a string.

ValueError

For an unsupported suffix; an empty key, or a key on a file that holds one array; a container whose signal array cannot be identified (the message lists the candidates), or a key that matches no array, several arrays, or one that is not numeric; a text file that is not a numeric table, or a 2-column table whose first column is not a strictly increasing time; or no numeric data (an empty or header-only table, an empty or 0-d array, or a non-numeric dtype such as strings or bool).

ImportError

For FLAC/OGG without soundfile, or HDF5 (including MATLAB v7.3 .mat) without h5py.

Source code in src/tokeye/io.py
def load_signal(
    path: str | Path, key: str | None = None
) -> tuple[np.ndarray, float | None]:
    """Load a 1D signal or 2D spectrogram and its sampling rate.

    Parameters
    ----------
    path
        A file with one of the suffixes in :data:`SIGNAL_SUFFIXES`.
    key
        Which array to read from a container (``.npz``, ``.mat``, ``.h5``,
        ``.hdf5``): an entry name, or an HDF5 dataset path (``/`` first for
        an exact path from the root) or its trailing whole path components,
        such as ``tree/pointname/data``. By default, the one array named
        ``data``, ``signal``, ``x``, ``spectrogram``, ``values`` or ``y``,
        else the only numeric array with more than one element.

    Returns
    -------
    tuple
        ``(array, fs)``; ``(1, N)`` and ``(N, 1)`` arrays are flattened to
        1D, and ``fs`` is ``None`` when the file does not record it. A
        2-column ``.csv``/``.txt`` is read as (time, value), with time in
        seconds unless the header names the column in ``ms`` or ``us``.

    Raises
    ------
    FileNotFoundError
        If ``path`` is not a file.
    TypeError
        ``key`` is not a string.
    ValueError
        For an unsupported suffix; an empty ``key``, or a ``key`` on a file
        that holds one array; a container whose signal array cannot be
        identified (the message lists the candidates), or a ``key`` that
        matches no array, several arrays, or one that is not numeric; a
        text file that is not a numeric table, or a 2-column table whose
        first column is not a strictly increasing time; or no numeric data
        (an empty or header-only table, an empty or 0-d array, or a
        non-numeric dtype such as strings or bool).
    ImportError
        For FLAC/OGG without ``soundfile``, or HDF5 (including MATLAB v7.3
        ``.mat``) without ``h5py``.
    """
    path = Path(path)
    if not path.is_file():
        raise FileNotFoundError(f"Input file not found: {path}")
    if key is not None:
        _check_key(key)
    suffix = path.suffix.lower()
    if suffix in _CONTAINER_LOADERS:
        data, fs = _CONTAINER_LOADERS[suffix](path, key)
    elif suffix not in _LOADERS:
        raise ValueError(
            f"unsupported input format {path.suffix!r} ({path}); supported: "
            f"{', '.join(SIGNAL_SUFFIXES)}"
        )
    elif key is not None:
        raise ValueError(
            f"{path}: key= selects an array inside .npz, .mat, .h5 and .hdf5 "
            f"files; a {suffix} file holds one array"
        )
    else:
        data, fs = _LOADERS[suffix](path)
    if fs is None:
        fs = fs_from_name(path)
    arr = _squeeze_vector(np.asarray(data))
    detail = _no_data(arr)
    if detail is not None:
        raise ValueError(f"{path}: no numeric data ({detail})")
    return arr, fs

Model registry, Hugging Face downloads and local checkpoint loading.

Gradio-free. Model architectures are imported lazily by their builders, so torchvision is only needed for ae_tf_maskrcnn (the ae extra).

DownloadError

Bases: OSError

Hugging Face could not be reached, and the weights are not cached.

It means what :class:huggingface_hub.errors.LocalEntryNotFoundError means, for the network failures huggingface_hub does not map to it.

Source code in src/tokeye/hub.py
class DownloadError(OSError):
    """Hugging Face could not be reached, and the weights are not cached.

    It means what :class:`huggingface_hub.errors.LocalEntryNotFoundError`
    means, for the network failures huggingface_hub does not map to it.
    """

ModelSpec dataclass

A registered model.

task is "segmentation" (TokEye, tokeye run) or "instance" (tokeye alfvenspec); channels names the mask channels of a segmentation model; size_mb is the download size.

Source code in src/tokeye/hub.py
@dataclass(frozen=True)
class ModelSpec:
    """A registered model.

    ``task`` is ``"segmentation"`` (``TokEye``, ``tokeye run``) or
    ``"instance"`` (``tokeye alfvenspec``); ``channels`` names the mask
    channels of a segmentation model; ``size_mb`` is the download size.
    """

    name: str
    filename: str  # file in the HF repo
    builder: Callable[[], nn.Module]
    repo_id: str | None = None  # None -> DEFAULT_REPO_ID (TOKEYE_HF_REPO override)
    task: str = "segmentation"
    channels: tuple[str, ...] = DEFAULT_CHANNELS
    size_mb: int = 0

cached_path(name)

Local path of a registry model's weights, or None if not cached.

Source code in src/tokeye/hub.py
def cached_path(name: str) -> Path | None:
    """Local path of a registry model's weights, or ``None`` if not cached."""
    spec = _spec(name)
    cached = try_to_load_from_cache(repo_for(name), spec.filename)
    return Path(cached) if isinstance(cached, str) else None

channels_for(source)

Mask channel names for a model (the default names for local files).

Source code in src/tokeye/hub.py
def channels_for(source: str | Path) -> tuple[str, ...]:
    """Mask channel names for a model (the default names for local files)."""
    spec = MODEL_REGISTRY.get(str(source))
    return spec.channels if spec is not None else DEFAULT_CHANNELS

download_model(name=DEFAULT_MODEL, repo_id=None)

Download (or find in the cache) a registry model's weights.

Raises:

Type Description
ValueError

name is not a registry name.

(LocalEntryNotFoundError, DownloadError)

The hub cannot be reached and the weights are not cached.

HfHubHTTPError

The hub answered with an error (a missing repo, for example).

Source code in src/tokeye/hub.py
def download_model(name: str = DEFAULT_MODEL, repo_id: str | None = None) -> Path:
    """Download (or find in the cache) a registry model's weights.

    Raises
    ------
    ValueError
        ``name`` is not a registry name.
    huggingface_hub.errors.LocalEntryNotFoundError, DownloadError
        The hub cannot be reached and the weights are not cached.
    huggingface_hub.errors.HfHubHTTPError
        The hub answered with an error (a missing repo, for example).
    """
    spec = _spec(name)
    repo = repo_id or repo_for(name)
    try:
        return Path(hf_hub_download(repo, spec.filename))
    except (LocalEntryNotFoundError, HfHubHTTPError):
        # First: HfHubHTTPError is an httpx2.HTTPError on huggingface_hub 2.x
        # and an httpx.HTTPError on 1.x; on 0.x both are RequestExceptions.
        raise
    except _network_errors() as exc:
        raise DownloadError(
            f"could not download {spec.filename} from {repo}: {exc}"
        ) from exc

is_cached(name)

Whether a registry model's weights are already downloaded.

Source code in src/tokeye/hub.py
def is_cached(name: str) -> bool:
    """Whether a registry model's weights are already downloaded."""
    return cached_path(name) is not None

load_model(source=DEFAULT_MODEL, device='auto')

Load a registry model (downloading it once) or a local checkpoint.

Parameters:

Name Type Description Default
source str | Path

A registry name (see :data:MODEL_REGISTRY) or a path to a local file: a .pt state dict; a legacy pickled module; a TorchScript archive (torch.jit.save), used as saved: trace in eval mode; segmentation models only; or a .pt2 exported program.

DEFAULT_MODEL
device str

"auto" (CUDA, then MPS, then CPU), "cpu", "cuda", "cuda:N" or "mps" (see :func:resolve_device).

'auto'

Raises:

Type Description
ValueError

An unknown or unavailable device, an unknown registry name, an unreadable checkpoint, or a state dict that fits no registered architecture.

FileNotFoundError

source looks like a .pt/.pt2 path that does not exist.

(LocalEntryNotFoundError, DownloadError)

The hub cannot be reached and the weights are not cached.

HfHubHTTPError

The hub answered with an error.

Source code in src/tokeye/hub.py
def load_model(source: str | Path = DEFAULT_MODEL, device: str = "auto") -> nn.Module:
    """Load a registry model (downloading it once) or a local checkpoint.

    Parameters
    ----------
    source
        A registry name (see :data:`MODEL_REGISTRY`) or a path to a local
        file: a ``.pt`` state dict; a legacy pickled module; a TorchScript
        archive (``torch.jit.save``), used as saved: trace in eval mode;
        segmentation models only; or a ``.pt2`` exported program.
    device
        ``"auto"`` (CUDA, then MPS, then CPU), ``"cpu"``, ``"cuda"``,
        ``"cuda:N"`` or ``"mps"`` (see :func:`resolve_device`).

    Raises
    ------
    ValueError
        An unknown or unavailable device, an unknown registry name, an
        unreadable checkpoint, or a state dict that fits no registered
        architecture.
    FileNotFoundError
        ``source`` looks like a ``.pt``/``.pt2`` path that does not exist.
    huggingface_hub.errors.LocalEntryNotFoundError, DownloadError
        The hub cannot be reached and the weights are not cached.
    huggingface_hub.errors.HfHubHTTPError
        The hub answered with an error.
    """
    resolved_device = resolve_device(device)
    name = str(source)

    if name in MODEL_REGISTRY:
        return _load_from_registry(name, resolved_device)

    path = Path(source)
    if not path.exists():
        if path.suffix in _PATH_SUFFIXES:
            raise FileNotFoundError(f"Model file not found: {name}")
        raise ValueError(
            f"Unknown model {name!r}; valid registry names: {sorted(MODEL_REGISTRY)}"
        )

    if path.suffix == ".pt2":
        return _load_pt2(path, name, resolved_device)
    if path.suffix == ".pt":
        return _load_pt(path, name, resolved_device)

    raise ValueError(f"Unsupported model file suffix: {path.suffix!r}")

model_label(source)

The name results record for a model: never a local path.

Parameters:

Name Type Description Default
source str | Path

A registry name or the path of a local checkpoint.

required

Returns:

Type Description
str

A registry name unchanged, otherwise Path(source).name (the file name, without the directories that lead to it).

Source code in src/tokeye/hub.py
def model_label(source: str | Path) -> str:
    """The name results record for a model: never a local path.

    Parameters
    ----------
    source
        A registry name or the path of a local checkpoint.

    Returns
    -------
    str
        A registry name unchanged, otherwise ``Path(source).name`` (the
        file name, without the directories that lead to it).
    """
    name = str(source)
    return name if name in MODEL_REGISTRY else Path(source).name

model_names(task=None)

Registry names, in registry order, optionally only for one task.

Source code in src/tokeye/hub.py
def model_names(task: str | None = None) -> list[str]:
    """Registry names, in registry order, optionally only for one ``task``."""
    return [n for n, s in MODEL_REGISTRY.items() if task is None or s.task == task]

repo_for(name)

The Hugging Face repo a registry name downloads from and is cached under.

That is the spec's repo_id, else :data:DEFAULT_REPO_ID (the TOKEYE_HF_REPO environment variable, read at import, else nc1/big_tf_unet). An unknown name gets :data:DEFAULT_REPO_ID.

Source code in src/tokeye/hub.py
def repo_for(name: str) -> str:
    """The Hugging Face repo a registry name downloads from and is cached under.

    That is the spec's ``repo_id``, else :data:`DEFAULT_REPO_ID` (the
    ``TOKEYE_HF_REPO`` environment variable, read at import, else
    ``nc1/big_tf_unet``). An unknown name gets :data:`DEFAULT_REPO_ID`.
    """
    spec = MODEL_REGISTRY.get(str(name))
    if spec is not None and spec.repo_id is not None:
        return spec.repo_id
    return DEFAULT_REPO_ID

require_task(source, task)

Raise ValueError if registry model source is not a task model.

Local checkpoint paths are not checked here (their task is only known once loaded); :func:tokeye.inference.infer rejects detection outputs.

Source code in src/tokeye/hub.py
def require_task(source: str | Path, task: str) -> None:
    """Raise ``ValueError`` if registry model ``source`` is not a ``task`` model.

    Local checkpoint paths are not checked here (their task is only known
    once loaded); :func:`tokeye.inference.infer` rejects detection outputs.
    """
    spec = MODEL_REGISTRY.get(str(source))
    if spec is None or spec.task == task:
        return
    hint = {
        "instance": "use `tokeye alfvenspec` (Python: tokeye.alfvenspec.detect)",
        "segmentation": "use `tokeye run` (Python: tokeye.TokEye)",
    }[spec.task]
    raise ValueError(
        f"{spec.name!r} is {_ARTICLES[spec.task]} {spec.task} model, but this needs "
        f"{_ARTICLES.get(task, 'a')} {task} model; {hint}"
    )

resolve_device(device='auto')

Check device and return it; "auto" picks one.

Parameters:

Name Type Description Default
device str | device

"auto" (CUDA, then MPS, then CPU), "cpu", "cuda", "cuda:N" or "mps", or a :class:torch.device of these.

'auto'

Returns:

Type Description
str

The device "auto" picks, else str(device) unchanged.

Raises:

Type Description
ValueError

Any other value, or a device this machine does not have.

Source code in src/tokeye/hub.py
def resolve_device(device: str | torch.device = "auto") -> str:
    """Check ``device`` and return it; ``"auto"`` picks one.

    Parameters
    ----------
    device
        ``"auto"`` (CUDA, then MPS, then CPU), ``"cpu"``, ``"cuda"``,
        ``"cuda:N"`` or ``"mps"``, or a :class:`torch.device` of these.

    Returns
    -------
    str
        The device ``"auto"`` picks, else ``str(device)`` unchanged.

    Raises
    ------
    ValueError
        Any other value, or a device this machine does not have.
    """
    text = str(device)
    if text == "auto":
        if torch.cuda.is_available():
            return "cuda"
        if _mps_available():
            return "mps"
        return "cpu"
    if text == "cpu":
        return text
    cuda = _CUDA_DEVICE.fullmatch(text)
    if cuda is not None:
        if not torch.cuda.is_available():
            raise ValueError(f"device {text!r}: CUDA is not available{_NO_DEVICE_HINT}")
        count = torch.cuda.device_count()
        if cuda.group(1) is not None and int(cuda.group(1)) >= count:
            raise ValueError(
                f"device {text!r}: this machine has {count} CUDA device(s), "
                f"numbered from 0{_NO_DEVICE_HINT}"
            )
        return text
    if text == "mps":
        if not _mps_available():
            raise ValueError(f"device {text!r}: MPS is not available{_NO_DEVICE_HINT}")
        return text
    raise ValueError(f"unknown device {text!r}; use {DEVICE_FORMS}")

weights_info(source)

Which weights a model source stands for, as results record them.

Parameters:

Name Type Description Default
source str | Path

A registry name, the path of a local checkpoint, or anything else (a caller's free-form model name).

required

Returns:

Type Description
dict or None

For a registry name, {"repo", "filename", "revision", "sha256"}: the Hugging Face repo (:func:repo_for) and file name, the cached snapshot's revision and the file's SHA-256 (both None when the weights are not cached). For a local file, {"name", "sha256"}: its file name only, never its directories. None for anything else (a directory, a missing path, a free-form name).

Notes

It never raises for a file that cannot be read: an OSError while hashing gives "sha256": None (logged at DEBUG). Digests are cached per path, modification time and size, so a batch hashes each checkpoint once.

Source code in src/tokeye/hub.py
def weights_info(source: str | Path) -> dict[str, str | None] | None:
    """Which weights a model source stands for, as results record them.

    Parameters
    ----------
    source
        A registry name, the path of a local checkpoint, or anything else
        (a caller's free-form model name).

    Returns
    -------
    dict or None
        For a registry name, ``{"repo", "filename", "revision", "sha256"}``:
        the Hugging Face repo (:func:`repo_for`) and file name, the cached
        snapshot's revision and the file's SHA-256 (both ``None`` when the
        weights are not cached). For a local file, ``{"name", "sha256"}``:
        its file name only, never its directories. ``None`` for anything
        else (a directory, a missing path, a free-form name).

    Notes
    -----
    It never raises for a file that cannot be read: an ``OSError`` while
    hashing gives ``"sha256": None`` (logged at DEBUG). Digests are cached
    per path, modification time and size, so a batch hashes each checkpoint
    once.
    """
    name = str(source)
    spec = MODEL_REGISTRY.get(name)
    if spec is not None:
        try:
            path = cached_path(name)
        except (OSError, ValueError) as exc:
            logger.debug("cannot look up the cached weights of %s: %s", name, exc)
            path = None
        revision = sha256 = None
        if path is not None:
            # The snapshot entry, not the blob it links to, names the revision.
            if path.parent.parent.name == "snapshots":
                revision = path.parent.name
            sha256 = _file_digest(path)
        return {
            "repo": repo_for(name),
            "filename": spec.filename,
            "revision": revision,
            "sha256": sha256,
        }
    path = Path(source)
    try:
        is_file = path.is_file()
    except OSError:
        is_file = False
    if not is_file:
        return None
    return {"name": path.name, "sha256": _file_digest(path)}

Headless batch inference: run TokEye over a list of files with no GUI.

Never imports gradio or matplotlib.pyplot (previews are drawn on a bare Figure), so this module is safe on HPC login/compute nodes and in CI.

check_key_inputs(paths, key)

Reject a key that some inputs cannot use.

key= selects an array inside a container, so every input must be a .npz, .mat, .h5 or .hdf5 file. Nothing is read.

Parameters:

Name Type Description Default
paths Sequence[str | Path]

The inputs, e.g. from :func:collect_inputs.

required
key str | None

The array to read from each input; None checks nothing.

required

Raises:

Type Description
TypeError

key is not a string.

ValueError

key is empty, or some inputs are not containers. The one-line message names them as given, in input order (the first 5, then a count).

Source code in src/tokeye/batch.py
def check_key_inputs(paths: Sequence[str | Path], key: str | None) -> None:
    """Reject a ``key`` that some inputs cannot use.

    ``key=`` selects an array inside a container, so every input must be a
    ``.npz``, ``.mat``, ``.h5`` or ``.hdf5`` file. Nothing is read.

    Parameters
    ----------
    paths
        The inputs, e.g. from :func:`collect_inputs`.
    key
        The array to read from each input; ``None`` checks nothing.

    Raises
    ------
    TypeError
        ``key`` is not a string.
    ValueError
        ``key`` is empty, or some inputs are not containers. The one-line
        message names them as given, in input order (the first 5, then a
        count).
    """
    if key is None:
        return
    _check_key(key)
    bad = [str(p) for p in paths if Path(p).suffix.lower() not in CONTAINER_SUFFIXES]
    if bad:
        raise ValueError(
            "key= (--key) applies only to .npz, .mat, .h5 and .hdf5 inputs, "
            f"not: {_join_limited(bad, sep=', ')}"
        )

check_unique_stems(paths)

Reject inputs whose outputs would overwrite each other.

Every per-input output is named after the file stem, so shot.npy and shot.wav, or a/shot.npy and b/shot.npy, would write the same files. Stems are compared ignoring case (Shot and shot are one file on the default macOS and Windows file systems).

Parameters:

Name Type Description Default
paths Sequence[Path]

The inputs, e.g. from :func:collect_inputs.

required

Raises:

Type Description
ValueError

Two or more inputs share a stem. The one-line message names each group of inputs (the first 5, then a count).

Source code in src/tokeye/batch.py
def check_unique_stems(paths: Sequence[Path]) -> None:
    """Reject inputs whose outputs would overwrite each other.

    Every per-input output is named after the file stem, so ``shot.npy`` and
    ``shot.wav``, or ``a/shot.npy`` and ``b/shot.npy``, would write the same
    files. Stems are compared ignoring case (``Shot`` and ``shot`` are one
    file on the default macOS and Windows file systems).

    Parameters
    ----------
    paths
        The inputs, e.g. from :func:`collect_inputs`.

    Raises
    ------
    ValueError
        Two or more inputs share a stem. The one-line message names each
        group of inputs (the first 5, then a count).
    """
    groups: dict[str, list[Path]] = {}
    for path in map(Path, paths):
        groups.setdefault(path.stem.casefold(), []).append(path)
    clashes = [
        f"{', '.join(map(str, members))} -> {members[0].stem!r}"
        for members in groups.values()
        if len(members) > 1
    ]
    if clashes:
        raise ValueError(
            "inputs would overwrite each other's outputs (outputs are named "
            f"after the file stem, ignoring case): {_join_limited(clashes, sep='; ')}"
            ". Process files that share a stem in separate runs with different "
            "output directories, or rename them."
        )

collect_inputs(inputs)

Expand a list of files/directories/glob patterns into concrete paths.

Each item is resolved as:

  • an existing file: kept as-is, whatever its suffix (an unsupported one then fails in :func:tokeye.io.load_signal);
  • an existing directory: its files with a suffix in :data:tokeye.io.DIRECTORY_SUFFIXES, sorted;
  • otherwise a glob pattern: its matches that are files with a suffix in :data:tokeye.io.SIGNAL_SUFFIXES (.csv/.txt included), sorted.

A file reached twice under the same name (through its directory and by name, or by a relative and an absolute path) is kept once, in its first-seen position and spelling. A symlink with another name stays a separate input.

Raises:

Type Description
ValueError

Nothing was collected. The message counts the glob matches that were skipped for their suffix, if any.

Source code in src/tokeye/batch.py
def collect_inputs(inputs: list[str]) -> list[Path]:
    """Expand a list of files/directories/glob patterns into concrete paths.

    Each item is resolved as:

    - an existing file: kept as-is, whatever its suffix (an unsupported one
      then fails in :func:`tokeye.io.load_signal`);
    - an existing directory: its files with a suffix in
      :data:`tokeye.io.DIRECTORY_SUFFIXES`, sorted;
    - otherwise a glob pattern: its matches that are files with a suffix in
      :data:`tokeye.io.SIGNAL_SUFFIXES` (``.csv``/``.txt`` included), sorted.

    A file reached twice under the same name (through its directory and by
    name, or by a relative and an absolute path) is kept once, in its
    first-seen position and spelling. A symlink with another name stays a
    separate input.

    Raises
    ------
    ValueError
        Nothing was collected. The message counts the glob matches that
        were skipped for their suffix, if any.
    """
    collected: list[Path] = []
    skipped: dict[Path, str] = {}  # resolved path -> suffix, for the error
    for item in inputs:
        path = Path(item)
        if path.is_file():
            found = [path]
        elif path.is_dir():
            found = sorted(
                p
                for p in path.iterdir()
                if p.is_file() and p.suffix.lower() in DIRECTORY_SUFFIXES
            )
        else:
            # glob.glob (not Path.glob) so absolute patterns keep working:
            # Path(".").glob() rejects non-relative patterns outright.
            matches = sorted(Path(match) for match in glob.glob(item))  # noqa: PTH207
            files = [p for p in matches if p.is_file()]
            found = [p for p in files if p.suffix.lower() in SIGNAL_SUFFIXES]
            for p in files:
                if p.suffix.lower() not in SIGNAL_SUFFIXES:
                    skipped[p.resolve()] = p.suffix.lower() or "(none)"
        collected.extend(found)

    seen: set[tuple[Path, str]] = set()
    result: list[Path] = []
    for path in collected:
        key = (path.resolve(), path.name.casefold())
        if key not in seen:
            seen.add(key)
            result.append(path)

    if not result:
        detail = ""
        if skipped:
            count = len(skipped)
            detail = (
                f" ({count} {'match' if count == 1 else 'matches'} skipped with "
                f"unsupported suffixes: {', '.join(sorted(set(skipped.values())))}; "
                f"supported: {', '.join(SIGNAL_SUFFIXES)})"
            )
        raise ValueError(f"No input files found for: {inputs}{detail}")

    return result

load_input(path, stft_kwargs, log=False)

Pre-1.0 helper: the float64 spectrogram for path.

stft_kwargs are :class:SpectrogramConfig fields; an fs key is checked and otherwise ignored (it only labels axes, and this returns none). The model sees these values cast to float32; use :func:load_spectrogram for that float32 input together with its axes.

Source code in src/tokeye/batch.py
def load_input(path: Path, stft_kwargs: dict, log: bool = False) -> np.ndarray:
    """Pre-1.0 helper: the float64 spectrogram for ``path``.

    ``stft_kwargs`` are :class:`SpectrogramConfig` fields; an ``fs`` key is
    checked and otherwise ignored (it only labels axes, and this returns
    none). The model sees these values cast to float32; use
    :func:`load_spectrogram` for that float32 input together with its axes.
    """
    data, _ = load_signal(path)
    cfg, fs = _split_fs({**stft_kwargs, "log": log}, None)
    _check_fs(fs)
    return _values64(data, cfg)[0]

load_spectrogram(path, config=None, *, fs=None, key=None)

Read path with :func:tokeye.io.load_signal and :func:prepare it.

An explicit fs wins over one found in the file. key selects the array in a container (see :func:tokeye.io.load_signal).

Source code in src/tokeye/batch.py
def load_spectrogram(
    path: str | Path,
    config: SpectrogramConfig | Mapping[str, Any] | None = None,
    *,
    fs: float | None = None,
    key: str | None = None,
) -> Spectrogram:
    """Read ``path`` with :func:`tokeye.io.load_signal` and :func:`prepare` it.

    An explicit ``fs`` wins over one found in the file. ``key`` selects the
    array in a container (see :func:`tokeye.io.load_signal`).
    """
    data, file_fs = load_signal(path, key=key)
    return prepare(data, config, fs=fs if fs is not None else file_fs)

process_file(path, model, config=None, out_dir=None, save_png=True, threshold=0.5, log=None, *, stft_kwargs=None, fs=None, fmt='npy', model_name=None, channels=None, tile='auto', key=None)

Segment one input file and write its outputs to out_dir.

Writes <stem>_mask.npy (float32 (C, H, W)) or, with fmt="npz", <stem>_tokeye.npz (a :class:Segmentation bundle); <stem>_preview.png unless save_png is off; and <stem>_params.json recording how the output was made. Returns the mask (or bundle) path.

tile is passed to :func:tokeye.inference.infer, and key to :func:tokeye.io.load_signal. params.json is removed before the first write and written last, so it only ever sits beside a complete set of outputs. A dict config is deprecated; an fs key in it moves to fs (ValueError when fs is given too).

model_name is the registry name or checkpoint path model was loaded from. params.json (and the fmt="npz" bundle) then record model as :func:tokeye.hub.model_label of it (a file name, never a local path) and weights as :func:tokeye.hub.weights_info of it, hashed after inference. Without it they record "model": "unknown" and "weights": null.

stft_kwargs is the deprecated 0.12.0 name of config: a dict of settings, which warns (DeprecationWarning, removed in 2.0) and moves an fs key to fs. Passing both raises TypeError. out_dir is required; its default only lets config be left out for that spelling.

Source code in src/tokeye/batch.py
def process_file(
    path: Path,
    model: nn.Module,
    config: SpectrogramConfig | dict | None = None,
    out_dir: Path | None = None,
    save_png: bool = True,
    threshold: float = 0.5,
    log: bool | None = None,
    *,
    stft_kwargs: Mapping[str, Any] | None = None,
    fs: float | None = None,
    fmt: str = "npy",
    model_name: str | Path | None = None,
    channels: tuple[str, ...] | None = None,
    tile: int | str | None = "auto",
    key: str | None = None,
) -> Path:
    """Segment one input file and write its outputs to ``out_dir``.

    Writes ``<stem>_mask.npy`` (float32 ``(C, H, W)``) or, with
    ``fmt="npz"``, ``<stem>_tokeye.npz`` (a :class:`Segmentation` bundle);
    ``<stem>_preview.png`` unless ``save_png`` is off; and
    ``<stem>_params.json`` recording how the output was made. Returns the
    mask (or bundle) path.

    ``tile`` is passed to :func:`tokeye.inference.infer`, and ``key`` to
    :func:`tokeye.io.load_signal`. ``params.json`` is removed before the
    first write and written last, so it only ever sits beside a complete set
    of outputs. A ``dict`` ``config`` is deprecated; an ``fs`` key in it
    moves to ``fs`` (``ValueError`` when ``fs`` is given too).

    ``model_name`` is the registry name or checkpoint path ``model`` was
    loaded from. ``params.json`` (and the ``fmt="npz"`` bundle) then record
    ``model`` as :func:`tokeye.hub.model_label` of it (a file name, never a
    local path) and ``weights`` as :func:`tokeye.hub.weights_info` of it,
    hashed after inference. Without it they record ``"model": "unknown"``
    and ``"weights": null``.

    ``stft_kwargs`` is the deprecated 0.12.0 name of ``config``: a dict of
    settings, which warns (``DeprecationWarning``, removed in 2.0) and
    moves an ``fs`` key to ``fs``. Passing both raises ``TypeError``.
    ``out_dir`` is required; its default only lets ``config`` be left out
    for that spelling.
    """
    if out_dir is None:
        raise TypeError("process_file() missing required argument: 'out_dir'")
    if stft_kwargs is not None:
        if config is not None:
            raise TypeError("pass either config or stft_kwargs, not both")
        config = dict(stft_kwargs)
    _check_fmt(fmt)
    check_tile(tile)
    path, out_dir = Path(path), Path(out_dir)
    cfg, fs = _coerce_config(config, fs)
    if log is not None:
        cfg = cfg.replace(log=log)
    spec = load_spectrogram(path, cfg, fs=fs, key=key)
    mask = infer(model, spec.values, tile=tile)
    plan = _plan_tiles(spec.values.shape, tile)  # the plan infer just used
    names = resolve_channels(channels or DEFAULT_CHANNELS, mask.shape[0])
    if model_name is None:
        label, weights = "unknown", None
    else:
        label, weights = hub.model_label(model_name), hub.weights_info(model_name)
    seg = Segmentation(mask, spec, names, label, weights)

    params_path = out_dir / f"{path.stem}_params.json"
    params_path.unlink(missing_ok=True)
    if fmt == "npz":
        output = seg.save(out_dir / f"{path.stem}_tokeye.npz")
    else:
        output = out_dir / f"{path.stem}_mask.npy"
        np.save(output, mask)

    if save_png:
        save_preview(seg, out_dir / f"{path.stem}_preview.png", threshold=threshold)

    params = {
        "tokeye_version": __version__,
        "model": label,
        "weights": weights,
        "device": str(_device_of(model)),
        "input": str(path),
        "key": key,
        "kind": spec.kind,
        "fs": spec.fs,
        "config": cfg.to_dict(),
        "mask_shape": list(mask.shape),
        "channels": list(names),
        "threshold": float(threshold),
        "tile": tile if tile is None or isinstance(tile, str) else int(tile),
        "tile_shape": None if plan is None else list(plan),
        "output": output.name,
        "created_utc": datetime.now(UTC).isoformat(),
    }
    _write_atomic(params_path, json.dumps(params, indent=2) + "\n")
    return output

process_files(paths, model, config, out_dir, *, save_png=True, threshold=0.5, fs=None, fmt='npy', tile='auto', model_name=None, channels=None, key=None, on_error=None)

Segment each path with a loaded model: the loop of :func:run_batch.

Each path goes through :func:process_file into out_dir, which must exist, under a progress bar. One input that fails does not stop the others. Inputs that share a stem are rejected before anything is written (:func:check_unique_stems).

Parameters:

Name Type Description Default
paths Sequence[Path]

Input files, e.g. from :func:collect_inputs.

required
model Module

A loaded segmentation model (:func:tokeye.hub.load_model).

required
config SpectrogramConfig | Mapping[str, Any] | None

Preprocessing settings (defaults when None). A dict is deprecated and warns once per call; an fs key in it moves to fs.

required
out_dir Path

Existing directory for the outputs.

required
save_png bool

As for :func:process_file.

True
threshold bool

As for :func:process_file.

True
fs bool

As for :func:process_file.

True
fmt bool

As for :func:process_file.

True
tile bool

As for :func:process_file.

True
channels bool

As for :func:process_file.

True
key bool

As for :func:process_file.

True
model_name str | Path | None

The registry name or checkpoint path model was loaded from, as for :func:process_file: each params.json records its label (:func:tokeye.hub.model_label) and weights (:func:tokeye.hub.weights_info). Without it they record "model": "unknown" and "weights": null.

None
on_error Callable[[Path, Exception], None] | None

Called as on_error(path, exc) for each input that fails. Without it, each failure is logged at ERROR (its traceback at DEBUG) on the tokeye.batch logger.

None

Returns:

Type Description
int

The number of inputs that failed.

Raises:

Type Description
ValueError

fmt is not "npy" or "npz", tile is below 512, or two inputs share a stem (their outputs would overwrite each other).

TypeError

tile is not "auto", None or an int.

Source code in src/tokeye/batch.py
def process_files(
    paths: Sequence[Path],
    model: nn.Module,
    config: SpectrogramConfig | Mapping[str, Any] | None,
    out_dir: Path,
    *,
    save_png: bool = True,
    threshold: float = 0.5,
    fs: float | None = None,
    fmt: str = "npy",
    tile: int | str | None = "auto",
    model_name: str | Path | None = None,
    channels: tuple[str, ...] | None = None,
    key: str | None = None,
    on_error: Callable[[Path, Exception], None] | None = None,
) -> int:
    """Segment each path with a loaded ``model``: the loop of :func:`run_batch`.

    Each path goes through :func:`process_file` into ``out_dir``, which must
    exist, under a progress bar. One input that fails does not stop the
    others. Inputs that share a stem are rejected before anything is
    written (:func:`check_unique_stems`).

    Parameters
    ----------
    paths
        Input files, e.g. from :func:`collect_inputs`.
    model
        A loaded segmentation model (:func:`tokeye.hub.load_model`).
    config
        Preprocessing settings (defaults when ``None``). A ``dict`` is
        deprecated and warns once per call; an ``fs`` key in it moves to
        ``fs``.
    out_dir
        Existing directory for the outputs.
    save_png, threshold, fs, fmt, tile, channels, key
        As for :func:`process_file`.
    model_name
        The registry name or checkpoint path ``model`` was loaded from, as
        for :func:`process_file`: each ``params.json`` records its label
        (:func:`tokeye.hub.model_label`) and weights
        (:func:`tokeye.hub.weights_info`). Without it they record
        ``"model": "unknown"`` and ``"weights": null``.
    on_error
        Called as ``on_error(path, exc)`` for each input that fails. Without
        it, each failure is logged at ERROR (its traceback at DEBUG) on the
        ``tokeye.batch`` logger.

    Returns
    -------
    int
        The number of inputs that failed.

    Raises
    ------
    ValueError
        ``fmt`` is not ``"npy"`` or ``"npz"``, ``tile`` is below 512, or two
        inputs share a stem (their outputs would overwrite each other).
    TypeError
        ``tile`` is not ``"auto"``, ``None`` or an int.
    """
    _check_fmt(fmt)
    check_tile(tile)
    cfg, fs = _coerce_config(config, fs)
    check_unique_stems(paths)
    failures = 0
    # disable=None: no bar when stderr is not a terminal (a SLURM log, say).
    for path in tqdm(paths, desc="tokeye run", disable=None):
        try:
            process_file(
                path,
                model,
                cfg,
                out_dir,
                save_png=save_png,
                threshold=threshold,
                fs=fs,
                fmt=fmt,
                model_name=model_name,
                channels=channels,
                tile=tile,
                key=key,
            )
        except Exception as exc:  # noqa: BLE001 - one bad file must not stop the batch
            failures += 1
            # Clear the progress bar first, or the line is glued onto it.
            with tqdm.external_write_mode(file=sys.stderr):
                if on_error is not None:
                    on_error(path, exc)
                else:
                    logger.error(
                        "Failed to process %s: %s: %s", path, type(exc).__name__, exc
                    )
                    logger.debug("traceback for %s", path, exc_info=exc)
    return failures

run_batch(inputs, model=hub.DEFAULT_MODEL, out_dir=Path('tokeye_output'), stft_kwargs=None, save_png=True, threshold=0.5, device='auto', log=None, *, config=None, fs=None, fmt='npy', tile='auto', key=None)

Segment every input, writing outputs to out_dir.

Parameters:

Name Type Description Default
inputs list[str]

Files, directories or glob patterns (see :func:collect_inputs).

required
model str | Path

Passed to :func:tokeye.hub.load_model; must be a segmentation model.

DEFAULT_MODEL
device str | Path

Passed to :func:tokeye.hub.load_model; must be a segmentation model.

DEFAULT_MODEL
config SpectrogramConfig | None

Preprocessing settings (defaults when omitted).

None
fs float | None

Sampling rate for every input (else read per file, if recorded).

None
fmt str

"npy" (mask only) or "npz" (full bundle).

'npy'
tile int | str | None

"auto" (default), None or an int >= 512; passed to :func:tokeye.inference.infer.

'auto'
key str | None

The array to read from each input, which must then be a .npz, .mat, .h5 or .hdf5 file (see :func:tokeye.io.load_signal); by default each file's own signal array.

None
stft_kwargs dict | None

Deprecated spellings of config. An fs key in stft_kwargs moves to fs.

None
log dict | None

Deprecated spellings of config. An fs key in stft_kwargs moves to fs.

None

Returns:

Type Description
int

The number of inputs that failed (each is logged, see :func:process_files).

Raises:

Type Description
ValueError

Bad settings, an fs that is not finite and positive (or given both in stft_kwargs and as fs), an empty key, an instance model, no inputs found, inputs that share a stem, or a key with inputs that are not containers (both checked before the model loads; see :func:check_unique_stems and :func:check_key_inputs), an unknown or unavailable device, or a model that cannot be loaded (see :func:tokeye.hub.load_model, which also lists its other errors).

TypeError

fs or key is of the wrong type, tile is not "auto", None or an int, or both config and stft_kwargs are given.

Source code in src/tokeye/batch.py
def run_batch(
    inputs: list[str],
    model: str | Path = hub.DEFAULT_MODEL,
    out_dir: Path = Path("tokeye_output"),
    stft_kwargs: dict | None = None,
    save_png: bool = True,
    threshold: float = 0.5,
    device: str = "auto",
    log: bool | None = None,
    *,
    config: SpectrogramConfig | None = None,
    fs: float | None = None,
    fmt: str = "npy",
    tile: int | str | None = "auto",
    key: str | None = None,
) -> int:
    """Segment every input, writing outputs to ``out_dir``.

    Parameters
    ----------
    inputs
        Files, directories or glob patterns (see :func:`collect_inputs`).
    model, device
        Passed to :func:`tokeye.hub.load_model`; must be a segmentation
        model.
    config
        Preprocessing settings (defaults when omitted).
    fs
        Sampling rate for every input (else read per file, if recorded).
    fmt
        ``"npy"`` (mask only) or ``"npz"`` (full bundle).
    tile
        ``"auto"`` (default), ``None`` or an int >= 512; passed to
        :func:`tokeye.inference.infer`.
    key
        The array to read from each input, which must then be a ``.npz``,
        ``.mat``, ``.h5`` or ``.hdf5`` file (see
        :func:`tokeye.io.load_signal`); by default each file's own signal
        array.
    stft_kwargs, log
        Deprecated spellings of ``config``. An ``fs`` key in
        ``stft_kwargs`` moves to ``fs``.

    Returns
    -------
    int
        The number of inputs that failed (each is logged, see
        :func:`process_files`).

    Raises
    ------
    ValueError
        Bad settings, an ``fs`` that is not finite and positive (or given
        both in ``stft_kwargs`` and as ``fs``), an empty ``key``, an
        instance model, no inputs found, inputs that share a
        stem, or a ``key`` with inputs that are not containers (both
        checked before the model loads; see :func:`check_unique_stems` and
        :func:`check_key_inputs`), an unknown or unavailable ``device``, or
        a model that cannot be loaded (see :func:`tokeye.hub.load_model`,
        which also lists its other errors).
    TypeError
        ``fs`` or ``key`` is of the wrong type, ``tile`` is not
        ``"auto"``, ``None`` or an int, or both ``config`` and
        ``stft_kwargs`` are given.
    """
    if stft_kwargs is not None or log is not None:
        warnings.warn(
            "run_batch(stft_kwargs=..., log=...) is deprecated; pass "
            "config=tokeye.SpectrogramConfig(...) (removed in 2.0)",
            DeprecationWarning,
            stacklevel=2,
        )
    if stft_kwargs is not None and config is not None:
        raise TypeError("pass either config or stft_kwargs, not both")
    _check_fmt(fmt)
    if key is not None:
        _check_key(key)
    check_tile(tile)
    if stft_kwargs is None:
        cfg = SpectrogramConfig.coerce(config)
    else:
        cfg, fs = _split_fs(stft_kwargs, fs)
    if log is not None:
        cfg = cfg.replace(log=log)
    _check_fs(fs)

    hub.require_task(model, "segmentation")
    paths = collect_inputs(inputs)
    check_unique_stems(paths)
    check_key_inputs(paths, key)
    loaded_model = hub.load_model(model, device)

    out_dir = Path(out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)

    return process_files(
        paths,
        loaded_model,
        cfg,
        out_dir,
        save_png=save_png,
        threshold=threshold,
        fs=fs,
        fmt=fmt,
        tile=tile,
        model_name=model,
        channels=hub.channels_for(model),
        key=key,
    )

save_overlay_png(spectrogram, mask, out_path, threshold=0.5, dpi=150)

Save a grayscale spectrogram with a semi-transparent mask overlay.

Coherent activity (mask[0]) is tinted green, transient activity (mask[1]) red, both thresholded at threshold.

Source code in src/tokeye/batch.py
def save_overlay_png(
    spectrogram: np.ndarray,
    mask: np.ndarray,
    out_path: Path,
    threshold: float = 0.5,
    dpi: int = 150,
) -> None:
    """Save a grayscale spectrogram with a semi-transparent mask overlay.

    Coherent activity (``mask[0]``) is tinted green, transient activity
    (``mask[1]``) red, both thresholded at ``threshold``.
    """
    from matplotlib.figure import Figure

    fig = Figure()
    ax = fig.add_subplot()
    ax.imshow(spectrogram, cmap="gray", origin="lower", aspect="auto")
    ax.imshow(overlay_rgba(mask, threshold), origin="lower", aspect="auto")
    fig.tight_layout()
    fig.savefig(out_path, dpi=dpi)

Shared "save results" backbone: npz bundle schemas.

Every TokEye surface that offers a "Save results" feature (Gradio tabs, the native Qt GUI, CLI batch) builds its output through this module so every saved .npz follows one consistent schema. numpy-only at import time: importing this module never pulls in torch, gradio, Qt, matplotlib, or plotly.

Two schemas:

  • tokeye-analysis/v1 — spectrogram surfaces (:func:analysis_bundle).
  • tokeye-modespec/v1 — mode-spectrogram surfaces (:func:modespec_bundle).

Both share four keys: schema, created_utc, source, params_json. Bundles are flat str -> np.ndarray-ish dicts saved with :func:save_npz; optional keys are simply absent when their inputs are absent — bundles never store None.

analysis_bundle(*, spectrogram, mask=None, stft_meta=None, raw=None, params=None, source='', axes=None)

Build a tokeye-analysis/v1 npz bundle (key -> array/str).

raw, when given, is a (t_ms, x) tuple of the raw signal trace, stored as raw_t_ms/raw_x. axes, when given, is the (time_ms, freq_khz) pair stored as is (float64); otherwise the axes come from :func:stft_axes applied to spectrogram's shape and stft_meta. Optional keys are simply omitted when their inputs are unavailable (never stored as None).

spectrogram must be 2D (H, W); mask, when given, must be (C, H, W) or (H, W) with the same H, W (ValueError otherwise, so a stale mask can never be saved next to a new input). axes must be 1D of lengths W and H, all finite, and cannot be combined with stft_meta (ValueError otherwise).

Source code in src/tokeye/export.py
def analysis_bundle(
    *,
    spectrogram: np.ndarray,
    mask: np.ndarray | None = None,
    stft_meta: dict | None = None,
    raw: tuple[np.ndarray, np.ndarray] | None = None,
    params: dict | None = None,
    source: str = "",
    axes: tuple[np.ndarray, np.ndarray] | None = None,
) -> NpzBundle:
    """Build a ``tokeye-analysis/v1`` npz bundle (key -> array/str).

    ``raw``, when given, is a ``(t_ms, x)`` tuple of the raw signal trace,
    stored as ``raw_t_ms``/``raw_x``. ``axes``, when given, is the
    ``(time_ms, freq_khz)`` pair stored as is (float64); otherwise the axes
    come from :func:`stft_axes` applied to ``spectrogram``'s shape and
    ``stft_meta``. Optional keys are simply omitted when their inputs are
    unavailable (never stored as ``None``).

    ``spectrogram`` must be 2D ``(H, W)``; ``mask``, when given, must be
    ``(C, H, W)`` or ``(H, W)`` with the same ``H, W`` (``ValueError``
    otherwise, so a stale mask can never be saved next to a new input).
    ``axes`` must be 1D of lengths ``W`` and ``H``, all finite, and cannot
    be combined with ``stft_meta`` (``ValueError`` otherwise).
    """
    if axes is not None and stft_meta is not None:
        raise ValueError("pass axes or stft_meta, not both")
    spectrogram = np.asarray(spectrogram, dtype=np.float32)
    if spectrogram.ndim != 2:
        raise ValueError(
            f"spectrogram must be 2D (H, W), got shape {spectrogram.shape}"
        )
    if mask is not None:
        mask = np.asarray(mask, dtype=np.float32)
        if mask.shape[-2:] != spectrogram.shape or mask.ndim not in (2, 3):
            raise ValueError(
                f"mask shape {mask.shape} does not match spectrogram shape "
                f"{spectrogram.shape}; expected (C, H, W) or (H, W)"
            )

    bundle: NpzBundle = {
        "schema": SCHEMA_ANALYSIS,
        "created_utc": _now_utc_iso(),
        "source": source,
        "params_json": _params_json(params),
        "spectrogram": spectrogram,
    }

    if mask is not None:
        bundle["mask"] = mask

    n_rows, n_cols = spectrogram.shape
    if axes is None:
        time_ms, freq_khz = stft_axes(n_rows, n_cols, stft_meta)
    else:
        time_ms, freq_khz = axes
        time_ms = _checked_axis("time_ms", time_ms, n_cols)
        freq_khz = _checked_axis("freq_khz", freq_khz, n_rows)
    if time_ms is not None:
        bundle["time_ms"] = time_ms
    if freq_khz is not None:
        bundle["freq_khz"] = freq_khz

    if raw is not None:
        raw_t_ms, raw_x = raw
        bundle["raw_t_ms"] = np.asarray(raw_t_ms, dtype=np.float32)
        bundle["raw_x"] = np.asarray(raw_x, dtype=np.float32)

    return bundle

default_stem(kind, *parts)

Build a timestamped default filename stem: tokeye_<kind>[_<part>...]_<YYYYmmdd-HHMMSS>. parts are stringified; falsy ones (None, "", 0, ...) are skipped.

Source code in src/tokeye/export.py
def default_stem(kind: str, *parts: object) -> str:
    """Build a timestamped default filename stem:
    ``tokeye_<kind>[_<part>...]_<YYYYmmdd-HHMMSS>``. ``parts`` are
    stringified; falsy ones (``None``, ``""``, ``0``, ...) are skipped."""
    bits = [str(part) for part in parts if part]
    stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
    return "_".join(["tokeye", kind, *bits, stamp])

modespec_bundle(*, result, nd=None, tok_mask=None, coh_thresh=None, params=None, source='')

Build a tokeye-modespec/v1 npz bundle from a mode-spectrogram analysis result (as returned by mode_spectrogram()): keys t_win_ms, freq_khz, n_dominant, coherence, n_range (2-seq), c95. nd, when given, is the TokEye-gated dominant-mode array and is stored as n_gated.

Source code in src/tokeye/export.py
def modespec_bundle(
    *,
    result: dict,
    nd: np.ndarray | None = None,
    tok_mask: np.ndarray | None = None,
    coh_thresh: float | None = None,
    params: dict | None = None,
    source: str = "",
) -> NpzBundle:
    """Build a ``tokeye-modespec/v1`` npz bundle from a mode-spectrogram
    analysis ``result`` (as returned by ``mode_spectrogram()``): keys
    ``t_win_ms``, ``freq_khz``, ``n_dominant``, ``coherence``, ``n_range``
    (2-seq), ``c95``. ``nd``, when given, is the TokEye-gated dominant-mode
    array and is stored as ``n_gated``.
    """
    bundle: NpzBundle = {
        "schema": SCHEMA_MODESPEC,
        "created_utc": _now_utc_iso(),
        "source": source,
        "params_json": _params_json(params),
        "n_dominant": np.asarray(result["n_dominant"], dtype=np.float32),
        "coherence": np.asarray(result["coherence"], dtype=np.float32),
        "t_win_ms": np.asarray(result["t_win_ms"], dtype=np.float64),
        "freq_khz": np.asarray(result["freq_khz"], dtype=np.float64),
        "n_range": np.asarray(result["n_range"], dtype=np.int64),
        "c95": np.float64(result["c95"]),
    }

    if nd is not None:
        bundle["n_gated"] = np.asarray(nd, dtype=np.float32)
    if tok_mask is not None:
        bundle["tok_mask"] = np.asarray(tok_mask, dtype=np.float32)
    if coh_thresh is not None:
        bundle["coh_thresh"] = np.float64(coh_thresh)

    return bundle

save_npz(path, bundle)

Save a bundle with np.savez_compressed; mkdir parents; return the actual path written (numpy appends .npz if not already present).

Source code in src/tokeye/export.py
def save_npz(path: str | Path, bundle: NpzBundle) -> Path:
    """Save a bundle with ``np.savez_compressed``; mkdir parents; return the
    actual path written (numpy appends ``.npz`` if not already present)."""
    path = Path(path)
    if not path.name.endswith(".npz"):
        path = path.with_name(path.name + ".npz")
    path.parent.mkdir(parents=True, exist_ok=True)
    np.savez_compressed(path, **bundle)
    return path

stft_axes(n_rows, n_cols, stft_meta)

Pixel-centre axes (time_ms, freq_khz) for an STFT image.

stft_meta keys: fs [Hz], t0_ms, n_fft, hop, clip_dc (bool). Math (must match the GUI/plotly renderers exactly)::

offset = 1 if clip_dc else 0
df = fs / n_fft / 1e3          # kHz per row
dt = hop / fs * 1e3            # ms per column
time_ms[i] = t0_ms + i * dt        (column centres, i in [0, n_cols))
freq_khz[r] = (r + offset) * df    (row centres, r in [0, n_rows))

For :func:tokeye.transforms.compute_stft output with t0_ms=0, time_ms[i] is column i's centre (to rounding).

Returns (None, None) when stft_meta is falsy or fs <= 0. Defaults when keys are absent: n_fft=1024, hop=128, t0_ms=0.0, clip_dc=True.

Source code in src/tokeye/export.py
def stft_axes(
    n_rows: int, n_cols: int, stft_meta: dict | None
) -> tuple[np.ndarray | None, np.ndarray | None]:
    """Pixel-centre axes (time_ms, freq_khz) for an STFT image.

    ``stft_meta`` keys: ``fs`` [Hz], ``t0_ms``, ``n_fft``, ``hop``,
    ``clip_dc`` (bool). Math (must match the GUI/plotly renderers exactly)::

        offset = 1 if clip_dc else 0
        df = fs / n_fft / 1e3          # kHz per row
        dt = hop / fs * 1e3            # ms per column
        time_ms[i] = t0_ms + i * dt        (column centres, i in [0, n_cols))
        freq_khz[r] = (r + offset) * df    (row centres, r in [0, n_rows))

    For :func:`tokeye.transforms.compute_stft` output with ``t0_ms=0``,
    ``time_ms[i]`` is column ``i``'s centre (to rounding).

    Returns ``(None, None)`` when ``stft_meta`` is falsy or ``fs <= 0``.
    Defaults when keys are absent: ``n_fft=1024``, ``hop=128``,
    ``t0_ms=0.0``, ``clip_dc=True``.
    """
    if not stft_meta:
        return None, None

    fs = stft_meta.get("fs", 0.0)
    if not fs or fs <= 0:
        return None, None

    n_fft = stft_meta.get("n_fft", _DEFAULT_N_FFT)
    hop = stft_meta.get("hop", _DEFAULT_HOP)
    t0_ms = stft_meta.get("t0_ms", _DEFAULT_T0_MS)
    clip_dc = stft_meta.get("clip_dc", _DEFAULT_CLIP_DC)

    offset = 1 if clip_dc else 0
    df = fs / n_fft / 1e3
    dt = hop / fs * 1e3

    time_ms = t0_ms + np.arange(n_cols, dtype=np.float64) * dt
    freq_khz = (np.arange(n_rows, dtype=np.float64) + offset) * df
    return time_ms, freq_khz

Deterministic synthetic example signal for demos and smoke tests.

Produces a 1D signal with Gaussian noise plus a handful of sustained frequency-ramp "chirps" (coherent activity) and short Gaussian-windowed "bursts" (transient activity), so a log1p spectrogram of the output shows both activity classes TokEye is trained to segment.

example_filename(fs)

The name of an example signal: tokeye_example_sr<fs>.npy.

The _sr suffix records the rate, so tokeye run reads it from the name (the app does not).

Parameters:

Name Type Description Default
fs float

Sampling rate [Hz].

required

Returns:

Type Description
str

The file name, e.g. tokeye_example_sr200000.npy.

Source code in src/tokeye/examples.py
def example_filename(fs: float) -> str:
    """The name of an example signal: ``tokeye_example_sr<fs>.npy``.

    The ``_sr`` suffix records the rate, so ``tokeye run`` reads it from the
    name (the app does not).

    Parameters
    ----------
    fs : float
        Sampling rate [Hz].

    Returns
    -------
    str
        The file name, e.g. ``tokeye_example_sr200000.npy``.
    """
    return f"tokeye_example_sr{format_rate(fs)}.npy"

format_rate(fs)

A sampling rate as it is written in file names and commands.

Parameters:

Name Type Description Default
fs float

Sampling rate [Hz].

required

Returns:

Type Description
str

"200000" for an integer rate (no .0), else str(fs).

Source code in src/tokeye/examples.py
def format_rate(fs: float) -> str:
    """A sampling rate as it is written in file names and commands.

    Parameters
    ----------
    fs : float
        Sampling rate [Hz].

    Returns
    -------
    str
        ``"200000"`` for an integer rate (no ``.0``), else ``str(fs)``.
    """
    return str(int(fs)) if float(fs).is_integer() else str(fs)

write_example_signal(directory, filename=None)

Write the default example signal into directory.

filename defaults to :func:example_filename of :data:DEFAULT_FS, the name tokeye example writes.

Source code in src/tokeye/examples.py
def write_example_signal(
    directory: Path,
    filename: str | None = None,
) -> Path:
    """Write the default example signal into ``directory``.

    ``filename`` defaults to :func:`example_filename` of :data:`DEFAULT_FS`,
    the name ``tokeye example`` writes.
    """
    directory = Path(directory)
    directory.mkdir(parents=True, exist_ok=True)
    out_path = directory / (filename or example_filename(DEFAULT_FS))
    np.save(out_path, make_example_signal())
    return out_path