unslothai/unsloth · error · SttUnavailableError

The dictation server is not running.

Error message

The dictation server is not running.

What it means

SttUnavailableError raised in transcribe_bytes() after load() returns: under _lock, self._port is None or _process_alive() is False. Something unloaded or killed the server in the gap between load() completing and this check — e.g. the idle-unload timer fired, another thread called unload(), or the process died on its own.

Source

Thrown at studio/backend/core/inference/stt_mtmd_sidecar.py:997

        model_id = resolve_mtmd_model_id(model)
        if cancel_event is not None and cancel_event.is_set():
            raise SttTranscriptionCancelledError("Transcription cancelled.")
        # No training guard here on purpose: load() starts the server with
        # -ngl 0 --no-mmproj-offload while a run is active, so this transcribes
        # on CPU exactly as whisper.cpp and Transformers do. Refusing after a
        # preload that succeeded only discarded the user's recording.
        # Reject a missing model before decoding, matching the other sidecars.
        self._ensure_model_downloaded(model_id)
        decoded_audio = _decode_audio_bounded(audio, cancel_event)
        if cancel_event is not None and cancel_event.is_set():
            raise SttTranscriptionCancelledError("Transcription cancelled.")
        wav_bytes = _pcm_to_wav_bytes(decoded_audio)
        audio_seconds = (len(decoded_audio) / _TARGET_SAMPLE_RATE) if len(decoded_audio) else None
        self.load(model_id, request_cancel_event = cancel_event)
        with self._lock:
            port = self._port
            if port is None or not self._process_alive():
                raise SttUnavailableError("The dictation server is not running.")
            # Another client can switch models in the gap between that load
            # returning and this lock, and the port read here would then be its
            # server. Refuse rather than transcribe on the wrong model.
            if self._model_id != model_id:
                raise SttModelBusyError(
                    "The dictation model changed while this recording was being "
                    "prepared. Try again."
                )
            # Long audio can outlast the keep-alive, and _post_transcribe runs
            # outside the lock, so disarm the timer rather than let it kill
            # llama-server mid-request and throw the dictation away.
            self._active_requests += 1
            self._cancel_idle_unload_locked()
        try:
            # Outside the lock: a held lock would block unload, including the
            # one a training run performs, for the whole request timeout.
            text = self._post_transcribe(
                port, model_id, wav_bytes, audio_seconds, cancel_event = cancel_event

View on GitHub (pinned to 203007d190)

Solutions

  1. Retry the transcription — the retry's load() will bring a fresh server up.
  2. If it recurs, check whether the idle-unload keep-alive is shorter than your decode time and raise it.
  3. Inspect llama-server logs for a crash (OOM, bad file) if no unload was requested.
Defensive patterns

Strategy: retry

Try / catch

```python
for attempt in range(2):
    try:
        return sidecar.transcribe_bytes(audio, cancel_event=ev)
    except SttUnavailableError as exc:
        if "not running" not in str(exc) or attempt:
            raise
        continue  # load() on retry brings a fresh server
```

Prevention

When it happens

Trigger: load() succeeds, then before the transcribe critical section runs, the server is unloaded (idle timeout, training preempt, crash) leaving _port None or a dead process.

Common situations: Idle keep-alive expiry racing a slow decode; training run unloading sidecars during audio preparation; llama-server crashing under OOM right after startup.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/e97a46b1c2b9122c. Report an issue: GitHub.