Ver Fonte

Added ROCm support for faster-whisper

Efren Yevale Varela há 1 semana atrás
pai
commit
58facc1c5b

+ 37 - 0
AGENTS.md

@@ -24,6 +24,43 @@ uv run stt-daemon setup && uv run stt-daemon run         # manual flow without s
 
 There is no CI, pre-commit, Makefile, or task runner. Run ruff + pytest manually before finishing.
 
+## ROCm environment (this host)
+
+The repo's lock/vendor pins CUDA wheels (`torch 2.14.0+cu130`, vendored `pywhispercpp`). After any
+`uv sync`, reinstall the ROCm stack; otherwise the venv silently goes back to CUDA. One command:
+
+```bash
+scripts/install-rocm.sh
+```
+
+It fetches the torch ROCm wheels and the CTranslate2 ROCm wheel. The manual equivalent, if the
+script cannot run on this host:
+
+```bash
+TMPDIR=$HOME/.tmp .venv/bin/pip install --force-reinstall torch torchvision torchaudio \
+  --index-url https://download.pytorch.org/whl/rocm7.2 \
+  --extra-index-url https://pypi.org/simple
+TMPDIR=$HOME/.tmp .venv/bin/pip install --force-reinstall ~/.tmp/ct2-rocm/ctranslate2-*-cp313-*.whl
+```
+
+- Use the CTranslate2 **ROCm** wheel (release v4.8.4 `rocm-python-wheels-Linux.zip`; the script
+  fetches it); PyPI's `ctranslate2` is CUDA-only.
+- Plain `uv run` re-syncs the venv implicitly, so it also reverts these wheels (`uv sync` is not
+  required to lose them). After installing the ROCm stack, run the suite as
+  `.venv/bin/python -m pytest` (or `uv run --no-sync ...`), and re-run `scripts/install-rocm.sh`
+  after any deliberate sync.
+- On HIP the default `hipMallocAsync` allocator silently corrupts CTranslate2 buffers
+  (OpenNMT/CTranslate2 #2090): wrong language detection, empty transcripts, hangs, or segfaults.
+  The transcriber sets `CT2_CUDA_ALLOCATOR=cub_caching` automatically on ROCm hosts (CTranslate2
+  4.8.4 makes it the HIP default); an explicit user-set value is respected.
+- STT backend: `[whisper] backend = "faster-whisper"`, `device = "auto"`, `compute_type = "int8"`.
+  `auto` resolves to the discrete R9700 (verified by VRAM delta) while the integrated GPU stays
+  idle, and auto language detection works on ROCm once the allocator is configured. The vendored
+  `pywhispercpp` wheel is CUDA-only and cannot import on this host.
+- **Do not set `HIP_VISIBLE_DEVICES=0` for CTranslate2** — it selects the wrong device and transcribes
+  garbage. Leave both devices visible; `device = "auto"` picks the R9700.
+- `TMPDIR=$HOME/.tmp` for pip/uv: `/tmp` is a 1 GiB tmpfs and large wheels fail with ENOSPC.
+
 ## Process architecture (don't break this)
 
 Three processes wired by UNIX sockets under `$XDG_RUNTIME_DIR` (fallback `/tmp`):

+ 18 - 2
docs/CONFIGURATION.md

@@ -40,8 +40,8 @@ trailing_trim_ms = 200                # Keep only this much trailing silence (le
 [whisper]
 backend                 = "faster-whisper"     # "faster-whisper" (CTranslate2) | "pywhispercpp" (whisper.cpp; see docs/PYWHISPERCPP.md)
 model                   = "small"               # "tiny"/"base"/"small"/"medium"/"large" (multilingual; do NOT use `.en` variants)
-device                  = "cpu"                 # "cpu" or "cuda"
-compute_type            = "int8"                # "int8" (CPU) or "float16" (CUDA, slower but more accurate)
+device                  = "cpu"                 # "cpu" | "cuda" | "auto" (see below)
+compute_type            = "int8"                # "int8" (CPU) | "float16"/"int8_float16" (CUDA/ROCm; see below)
 language                = "auto"                # "auto" (detect), "en", "es", or None/""/"none"
 beam_size               = 1                     # 1 = fast greedy, >1 = slower beam search
 vad_filter              = false                 # Use Whisper's built-in VAD (we already have webrtcvad)
@@ -211,6 +211,22 @@ wheel; see [PYWHISPERCPP.md](PYWHISPERCPP.md)). `model`, `language`, `beam_size`
 `faster-whisper`; `vad_filter` is ignored (with a warning) on `pywhispercpp`. For `pywhispercpp`
 the ggml quantization is chosen by the model name (e.g. `small`, `small-q5_1`).
 
+`device` values for `faster-whisper`:
+
+- `"cpu"` is the default.
+- `"cuda"` is explicit: the repo probes `torch.cuda.is_available()` and falls back to CPU
+  (`compute_type = "int8"`) with a warning when it is false.
+- `"auto"` is passed through to CTranslate2, which picks the first HIP/CUDA device; on this host
+  that is the discrete R9700. There is no repo-side `auto` branch.
+
+`compute_type` guidance: CPU `"int8"`; CUDA `"float16"` or `"int8_float16"`; ROCm accepts the same
+values, with `"float16"` measured fastest on the R9700 and `"int8"` close behind. GPU-only types
+(`"float16"`, `"int8_float16"`, `"bfloat16"`, `"int8_bfloat16"`) are downgraded to `"int8"` with a
+warning when the model resolves to CPU, because CTranslate2 rejects them there. CUDA and ROCm
+need the matching CTranslate2 wheel: PyPI's is CUDA-only, and ROCm needs the wheel installed by
+`scripts/install-rocm.sh`. A CUDA-only wheel on a ROCm host falls back to CPU silently, so check
+the resolved device in the daemon log.
+
 `max_prompt_tokens` was removed: the prompt budget is derived as
 `max_context_tokens - max_tokens - compaction_safety_margin`, so the window and the trim budget
 can never disagree. A legacy `max_prompt_tokens` key is ignored with a warning.

+ 37 - 4
docs/TROUBLESHOOTING.md

@@ -32,16 +32,48 @@ By default, the daemon uses `compute_type = "int8"` and `device = "cpu"`, which
 
 1. **Pin `cpu_threads` to the physical core count**: `cpu_threads = 16` in `[whisper]`
    (CTranslate2's automatic choice is often slower than an explicit count on many-core CPUs)
-2. **Reduce model size**: `--model base` or `--model tiny`
-3. **Enable GPU** (if you have CUDA): `--device cuda` with `compute_type float16`
+2. **Reduce model size**: `--model base` or `--model tiny` (the CLI `--model` is a valid override;
+   `--device` is not, it selects the audio capture device)
+3. **Use a GPU**: edit `[whisper]` in `config.toml` and set `device = "cuda"` (or `"auto"`) with
+   `compute_type = "float16"`. These are config keys, not CLI flags: `--device` overrides the audio
+   capture device and `--backend` chooses `parec`/`sounddevice`.
 4. **Reduce beam_size**: Edit config to `beam_size = 1` (already default, very fast)
 
 Example:
 
+```toml
+# config.toml
+[whisper]
+device = "auto"          # or "cuda"; "cpu" on a machine without a GPU
+compute_type = "float16" # "int8" for CPU
+```
+
+```bash
+uv run stt-daemon --model base
+```
+
+## GPU on AMD / ROCm
+
+A CUDA-only CTranslate2 wheel does not fail loudly on a ROCm host: it loads and silently runs on
+CPU, so transcription keeps working but stays slow. The transcriber now logs the resolved device at
+startup and warns when `device = "auto"` or `"cuda"` resolves to CPU while CTranslate2 reports no
+GPU device.
+
+**Fix:** install the ROCm stack after `uv sync`, then confirm the resolved device:
+
 ```bash
-uv run stt-daemon --model base --device cuda
+scripts/install-rocm.sh
+uv run stt-daemon --check
 ```
 
+Re-run `scripts/install-rocm.sh` after any later `uv sync`, or after a plain `uv run` (it syncs
+the venv implicitly), which puts the CUDA wheels back. Run the test suite as
+`.venv/bin/python -m pytest` on this host so it does not re-sync.
+
+For scale, a 25.9 s Spanish utterance with `large-v3` and `beam_size = 1` measured RTF ~0.37-0.43
+on CPU (auto language) versus ~0.08-0.11 on the R9700, and a 12-utterance mixed Spanish/English run
+auto-detected correctly on ROCm once the allocator was configured.
+
 ## "Transcription backlog reached N of M"
 
 This warning means the single transcription worker is falling behind: utterances
@@ -51,7 +83,8 @@ model isn't thread-safe), so fix it by making transcription faster rather than b
 queueing more:
 
 1. **Smaller/faster model**: `--model base` or `tiny`
-2. **GPU**: `--device cuda` with `compute_type float16`
+2. **GPU**: edit `[whisper]` in `config.toml`: `device = "cuda"` (or `"auto"`) and
+   `compute_type = "float16"`. There is no Whisper-level `--device` flag.
 3. **Raise `backlog_warn_at`** or `queue_size` in `[daemon]` only to make the
    warning/dropping less likely — it hides the symptom, not the cause.
 

+ 3 - 0
pyproject.toml

@@ -14,6 +14,9 @@ authors = [{name = "Efren", email = "efren@example.com"}]
 dependencies = [
     "numpy>=1.26",
     "faster-whisper>=1.1.0",
+    # Floor set for the HIP allocator fix (cub_caching default on ROCm); the ROCm
+    # host still needs the matching ROCm wheel, installed by scripts/install-rocm.sh.
+    "ctranslate2>=4.8.4",
     "webrtcvad-wheels>=2.0.14",
     "questionary>=2.0",
     "textual>=0.80",

+ 114 - 0
scripts/install-rocm.sh

@@ -0,0 +1,114 @@
+#!/usr/bin/env bash
+# Reinstall the AMD ROCm runtime stack (torch + CTranslate2 ROCm wheels) into the repo .venv.
+# Never creates or re-syncs the venv; run `uv sync` first, then this script.
+
+set -euo pipefail
+
+# Resolve the repo root from the script location
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(dirname "$SCRIPT_DIR")"
+
+CT2_VERSION="4.8.4"
+ROCM_INDEX="https://download.pytorch.org/whl/rocm7.2"
+
+help() {
+    cat << 'EOF'
+ROCm runtime installer (torch + CTranslate2 ROCm wheels into .venv)
+
+Usage: scripts/install-rocm.sh [--ct2-version X.Y.Z] [--rocm-index <torch index url>]
+
+Options:
+  --ct2-version X.Y.Z       CTranslate2 release version (default: 4.8.4)
+  --rocm-index <url>        Torch ROCm index URL (default: https://download.pytorch.org/whl/rocm7.2)
+  -h, --help                Show this message
+
+Example:
+  scripts/install-rocm.sh
+  scripts/install-rocm.sh --ct2-version 4.8.4 --rocm-index https://download.pytorch.org/whl/rocm7.2
+
+Note: any later `uv sync` reverts these wheels, so re-run this script afterwards.
+EOF
+}
+
+main() {
+    while [[ $# -gt 0 ]]; do
+        case "${1:-}" in
+            --ct2-version)
+                CT2_VERSION="${2:-}"
+                if [[ -z "$CT2_VERSION" ]]; then
+                    echo "Missing value for --ct2-version" >&2
+                    return 1
+                fi
+                shift 2
+                ;;
+            --rocm-index)
+                ROCM_INDEX="${2:-}"
+                if [[ -z "$ROCM_INDEX" ]]; then
+                    echo "Missing value for --rocm-index" >&2
+                    return 1
+                fi
+                shift 2
+                ;;
+            -h|--help|help)
+                help
+                return 0
+                ;;
+            *)
+                echo "Unknown argument: ${1:-}" >&2
+                echo "Use 'scripts/install-rocm.sh --help' for usage" >&2
+                return 1
+                ;;
+        esac
+    done
+
+    local venv_python="$REPO_ROOT/.venv/bin/python"
+    if [[ ! -x "$venv_python" ]]; then
+        echo "Error: $venv_python not found." >&2
+        echo "Run 'uv sync' first ('uv sync --extra speaker --extra triage' if you use the optional lanes), then re-run this script." >&2
+        return 1
+    fi
+
+    export TMPDIR="${TMPDIR:-$HOME/.tmp}"
+    mkdir -p "$TMPDIR"
+
+    local venv_pip="$REPO_ROOT/.venv/bin/pip"
+    "$venv_pip" install --force-reinstall torch torchvision torchaudio \
+        --index-url "$ROCM_INDEX" \
+        --extra-index-url "https://pypi.org/simple"
+
+    local workdir
+    workdir="$(mktemp -d "$TMPDIR/ct2-rocm-XXXXXX")"
+    trap 'rm -rf "${workdir:-}"' EXIT
+    mkdir -p "$workdir"
+
+    local bundle_url="https://github.com/OpenNMT/CTranslate2/releases/download/v${CT2_VERSION}/rocm-python-wheels-Linux.zip"
+    local bundle_zip="$workdir/rocm-python-wheels-Linux.zip"
+    curl -fsSL -o "$bundle_zip" "$bundle_url"
+    unzip -q -o "$bundle_zip" -d "$workdir"
+
+    local py_tag
+    py_tag="$("$venv_python" -c 'import sys; print(f"cp{sys.version_info.major}{sys.version_info.minor}")')"
+
+    # The release archive layout is not stable across versions: v4.8.0 puts the
+    # wheels at the top level, v4.8.4 nests them under temp-linux/. Search deeply.
+    local wheel
+    wheel="$(find "$workdir" -type f -name "ctranslate2-*-${py_tag}-*.whl" -print -quit)"
+    if [[ -z "$wheel" ]]; then
+        echo "Error: no CTranslate2 wheel for $py_tag found in $bundle_url" >&2
+        return 1
+    fi
+    "$venv_pip" install --force-reinstall "$wheel"
+
+    rm -rf "$workdir"
+    trap - EXIT
+
+    "$venv_python" -c 'import ctranslate2, torch; print(torch.__version__); print(torch.version.hip); print(ctranslate2.get_cuda_device_count()); import sys; sys.exit(1 if torch.version.hip is None or ctranslate2.get_cuda_device_count() == 0 else 0)'
+
+    cat << 'EOF'
+ROCm stack installed. Next step: run `uv run stt-daemon --check` to confirm
+(the transcriber/CLI now report the resolved device).
+Note: any later `uv sync` will revert these wheels, so re-run this script afterwards.
+EOF
+}
+
+main "$@"

+ 115 - 0
src/stt_daemon/accel.py

@@ -0,0 +1,115 @@
+"""GPU accelerator helpers for the faster-whisper backend.
+
+On AMD/ROCm hosts CTranslate2's default HIP allocator (``hipMallocAsync``)
+silently corrupts buffers (OpenNMT/CTranslate2 issue #2090). The symptom is
+wrong language detection, empty transcripts, hangs, or segfaults. Setting
+``CT2_CUDA_ALLOCATOR=cub_caching`` fixes it completely. CTranslate2 4.8.4
+makes this the default on HIP, but older ROCm wheels (4.8.0-4.8.3) are still
+in use, so the daemon sets the variable itself.
+
+The environment variable must be set *before* the CTranslate2 model is
+created: the allocator is chosen when the model's CTranslate2 context is
+built, so setting it afterwards has no effect. CUDA/NVIDIA hosts are left
+untouched (the variable is only set when a ROCm host is detected).
+"""
+
+import os
+import sys
+
+CT2_ALLOCATOR_ENV = "CT2_CUDA_ALLOCATOR"
+ROCM_CT2_ALLOCATOR = "cub_caching"
+
+GPU_ONLY_COMPUTE_TYPES = frozenset(
+    {"float16", "int8_float16", "bfloat16", "int8_bfloat16"}
+)
+CPU_COMPUTE_TYPE = "int8"
+
+_ROCM_MARKER_PATHS = ("/opt/rocm",)
+
+
+def cpu_safe_compute_type(compute_type: str) -> str:
+    """Return a compute type CTranslate2 accepts on CPU.
+
+    GPU-only types raise ``ValueError`` on CPU, so they become ``int8``.
+    """
+    return CPU_COMPUTE_TYPE if compute_type in GPU_ONLY_COMPUTE_TYPES else compute_type
+
+
+def is_rocm_host() -> bool:
+    """Return True when this host is an AMD/ROCm machine.
+
+    Never imports torch: an already-imported torch is inspected through
+    ``sys.modules`` instead, so this is cheap and safe to call on every start.
+    """
+    if "torch" in sys.modules:
+        torch = sys.modules["torch"]
+        version = getattr(torch, "version", None)
+        return getattr(version, "hip", None) is not None
+    if os.environ.get("ROCM_PATH") or os.environ.get("ROCM_HOME"):
+        return True
+    return any(os.path.isdir(path) for path in _ROCM_MARKER_PATHS)
+
+
+def configure_ct2_allocator() -> str | None:
+    """Select a working CTranslate2 allocator on ROCm hosts.
+
+    Returns ``None`` when the host is not ROCm (nothing is changed). On a
+    ROCm host an explicit user value is preserved; otherwise the allocator is
+    set to ``cub_caching`` and returned.
+
+    Must be called before the CTranslate2 model is created.
+    """
+    if not is_rocm_host():
+        return None
+    existing = os.environ.get(CT2_ALLOCATOR_ENV)
+    if existing is not None:
+        return existing
+    os.environ[CT2_ALLOCATOR_ENV] = ROCM_CT2_ALLOCATOR
+    return ROCM_CT2_ALLOCATOR
+
+
+def ct2_cuda_device_count() -> int:
+    """Number of CUDA/HIP devices CTranslate2 exposes (0 when unavailable)."""
+    try:
+        import ctranslate2
+
+        return ctranslate2.get_cuda_device_count()
+    except Exception:
+        return 0
+
+
+def resolved_device_info(
+    model: object, *, device: str, compute_type: str
+) -> tuple[str, str]:
+    """Read the device/compute type CTranslate2 actually resolved.
+
+    Faster-whisper's ``WhisperModel`` exposes the CTranslate2 model as
+    ``model.model``. Fakes without that attribute fall back to the configured
+    values instead of raising.
+    """
+    inner = getattr(model, "model", None)
+    return (
+        getattr(inner, "device", device),
+        getattr(inner, "compute_type", compute_type),
+    )
+
+
+def acceleration_warning(
+    configured_device: str, resolved_device: str, device_count: int
+) -> str | None:
+    """Warn when a GPU request silently resolved to CPU with no accelerator."""
+    if device_count != 0 or resolved_device != "cpu":
+        return None
+    if configured_device == "cuda":
+        return (
+            "device=cuda requested but CTranslate2 exposes no CUDA/HIP device; "
+            "install the matching CTranslate2 wheel (ROCm hosts: "
+            "scripts/install-rocm.sh)."
+        )
+    if configured_device == "auto":
+        return (
+            "device=auto resolved to CPU because CTranslate2 exposes no CUDA/HIP "
+            "device; install the matching CTranslate2 wheel (ROCm hosts: "
+            "scripts/install-rocm.sh)."
+        )
+    return None

+ 11 - 1
src/stt_daemon/cli.py

@@ -2,12 +2,14 @@
 
 import argparse
 import logging
+import os
 import signal
 import sys
 from pathlib import Path
 
 import numpy as np
 
+from .accel import CT2_ALLOCATOR_ENV
 from .audio import list_sources, open_capture
 from .bus import default_socket_path
 from .config import load_config
@@ -844,8 +846,16 @@ def _check(args) -> int:
 
     try:
         transcriber = build_transcriber(cfg.whisper)
+        device = getattr(transcriber, "device", cfg.whisper.device)
+        compute_type = getattr(transcriber, "compute_type", cfg.whisper.compute_type)
+        allocator = os.environ.get(CT2_ALLOCATOR_ENV)
+        allocator_suffix = f", ct2_allocator={allocator}" if allocator else ""
         transcriber.close()
-        print("✓ Model ready", flush=True)
+        print(
+            f"✓ Model ready (device={device}, compute_type={compute_type})"
+            f"{allocator_suffix}",
+            flush=True,
+        )
     except Exception as e:
         print(f"Error loading model: {e}", file=sys.stderr)
         return 1

+ 45 - 4
src/stt_daemon/transcriber.py

@@ -8,6 +8,14 @@ from typing import Protocol
 import numpy as np
 from faster_whisper import WhisperModel
 
+from .accel import (
+    acceleration_warning,
+    configure_ct2_allocator,
+    cpu_safe_compute_type,
+    ct2_cuda_device_count,
+    resolved_device_info,
+)
+
 logger = logging.getLogger(__name__)
 
 BACKEND_FASTER_WHISPER = "faster-whisper"
@@ -71,6 +79,8 @@ class Transcriber:
         self.language = language
         self.device = device
         self.compute_type = compute_type
+        # Configured value at entry, before any CPU fallback rewrites self.device.
+        self._configured_device = device
         self.beam_size = beam_size
         self.vad_filter = vad_filter
         self.cpu_threads = cpu_threads
@@ -86,17 +96,37 @@ class Transcriber:
             try:
                 import torch
                 if not torch.cuda.is_available():
-                    print(
-                        "CUDA requested but not available. Falling back to CPU.",
-                        flush=True,
+                    logger.warning(
+                        "CUDA requested but not available. Falling back to CPU."
                     )
                     self.device = "cpu"
                     self.compute_type = "int8"
             except ImportError:
-                print("torch not available. Using CPU.", flush=True)
+                logger.warning("torch not available. Using CPU.")
                 self.device = "cpu"
                 self.compute_type = "int8"
 
+        # The CT2 allocator env var must be set before the CTranslate2 model
+        # (and its context) exists; moving this below model creation is wrong.
+        allocator = configure_ct2_allocator()
+
+        device_count = ct2_cuda_device_count()
+        if self.device != "cpu" and device_count == 0:
+            warning = acceleration_warning(self._configured_device, "cpu", device_count)
+            if warning is not None:
+                logger.warning(warning)
+            self.device = "cpu"
+
+        if self.device == "cpu":
+            safe_compute_type = cpu_safe_compute_type(self.compute_type)
+            if safe_compute_type != self.compute_type:
+                logger.warning(
+                    "compute_type=%s needs a GPU; using %s on CPU.",
+                    self.compute_type,
+                    safe_compute_type,
+                )
+                self.compute_type = safe_compute_type
+
         self.model = WhisperModel(
             model,
             device=self.device,
@@ -104,6 +134,17 @@ class Transcriber:
             cpu_threads=self.cpu_threads,
         )
 
+        self.device, self.compute_type = resolved_device_info(
+            self.model, device=self.device, compute_type=self.compute_type
+        )
+        logger.info(
+            "whisper ready: model=%s device=%s compute_type=%s ct2_allocator=%s",
+            model,
+            self.device,
+            self.compute_type,
+            allocator or "default",
+        )
+
     def transcribe(self, audio: np.ndarray) -> TranscriptResult:
         """Transcribe audio to text.
 

+ 198 - 0
tests/test_accel.py

@@ -0,0 +1,198 @@
+"""Tests for the ROCm accelerator helpers."""
+
+import logging
+import os
+import sys
+import types
+
+import pytest
+
+from stt_daemon import accel
+from stt_daemon import transcriber as transcriber_mod
+from stt_daemon.transcriber import Transcriber
+
+
+def _fake_torch(*, hip=None, cuda_available=False):
+    return types.SimpleNamespace(
+        version=types.SimpleNamespace(hip=hip),
+        cuda=types.SimpleNamespace(is_available=lambda: cuda_available),
+    )
+
+
+class _RecordingWhisperModel:
+    """Records the allocator and arguments visible at construction time."""
+
+    last: "_RecordingWhisperModel | None" = None
+
+    def __init__(self, model, device, compute_type, cpu_threads=0):
+        self.model = None  # no CTranslate2 inner model in the fake
+        self.allocator_at_creation = os.environ.get(accel.CT2_ALLOCATOR_ENV)
+        self.device_arg = device
+        self.compute_type_arg = compute_type
+        _RecordingWhisperModel.last = self
+
+
+def test_is_rocm_host_true_when_torch_reports_hip(monkeypatch):
+    monkeypatch.setitem(sys.modules, "torch", _fake_torch(hip="7.2.0"))
+    assert accel.is_rocm_host() is True
+
+
+def test_is_rocm_host_false_when_torch_reports_no_hip(monkeypatch):
+    monkeypatch.setitem(sys.modules, "torch", _fake_torch(hip=None))
+    assert accel.is_rocm_host() is False
+
+
+def test_is_rocm_host_false_without_torch_env_or_marker(monkeypatch):
+    monkeypatch.delitem(sys.modules, "torch", raising=False)
+    monkeypatch.delenv("ROCM_PATH", raising=False)
+    monkeypatch.delenv("ROCM_HOME", raising=False)
+    monkeypatch.setattr(accel, "_ROCM_MARKER_PATHS", ())
+    assert accel.is_rocm_host() is False
+
+
+def test_is_rocm_host_true_when_marker_path_is_a_directory(monkeypatch, tmp_path):
+    monkeypatch.delitem(sys.modules, "torch", raising=False)
+    monkeypatch.delenv("ROCM_PATH", raising=False)
+    monkeypatch.delenv("ROCM_HOME", raising=False)
+    monkeypatch.setattr(accel, "_ROCM_MARKER_PATHS", (str(tmp_path),))
+    assert accel.is_rocm_host() is True
+
+
+def test_configure_ct2_allocator_is_noop_on_non_rocm_host(monkeypatch):
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: False)
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+
+    assert accel.configure_ct2_allocator() is None
+    assert accel.CT2_ALLOCATOR_ENV not in os.environ
+
+
+def test_configure_ct2_allocator_sets_cub_caching_on_rocm_host(monkeypatch):
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: True)
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+
+    assert accel.configure_ct2_allocator() == accel.ROCM_CT2_ALLOCATOR
+    assert os.environ[accel.CT2_ALLOCATOR_ENV] == "cub_caching"
+
+
+def test_configure_ct2_allocator_preserves_explicit_value(monkeypatch):
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: True)
+    monkeypatch.setenv(accel.CT2_ALLOCATOR_ENV, "default")
+
+    assert accel.configure_ct2_allocator() == "default"
+    assert os.environ[accel.CT2_ALLOCATOR_ENV] == "default"
+
+
+@pytest.mark.parametrize(
+    ("configured", "resolved", "count"),
+    [
+        ("cpu", "cpu", 0),
+        ("auto", "cuda", 1),
+    ],
+)
+def test_acceleration_warning_returns_none_when_healthy(configured, resolved, count):
+    assert accel.acceleration_warning(configured, resolved, count) is None
+
+
+def test_acceleration_warning_cuda_states_request_not_honored(monkeypatch):
+    message = accel.acceleration_warning("cuda", "cpu", 0)
+
+    assert message is not None
+    assert "device=cuda requested" in message
+    assert "no CUDA/HIP device" in message
+    assert "install-rocm.sh" in message
+    assert "\n" not in message
+
+
+def test_acceleration_warning_auto_names_missing_device(monkeypatch):
+    message = accel.acceleration_warning("auto", "cpu", 0)
+
+    assert message is not None
+    assert "device=auto resolved to CPU" in message
+    assert "no CUDA/HIP" in message
+    assert "install-rocm.sh" in message
+    assert "\n" not in message
+
+
+def test_transcriber_sets_allocator_before_model_creation(monkeypatch, caplog):
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: True)
+    monkeypatch.setattr(transcriber_mod, "ct2_cuda_device_count", lambda: 1)
+    monkeypatch.setattr(transcriber_mod, "WhisperModel", _RecordingWhisperModel)
+
+    with caplog.at_level(logging.INFO, logger="stt_daemon.transcriber"):
+        transcriber = Transcriber(
+            "small", device="cpu", compute_type="int8", language=None
+        )
+
+    assert _RecordingWhisperModel.last is not None
+    assert _RecordingWhisperModel.last.allocator_at_creation == "cub_caching"
+    assert transcriber.device == "cpu"
+    assert "device=cpu" in caplog.text
+    assert "ct2_allocator=cub_caching" in caplog.text
+
+
+def test_transcriber_cuda_fallback_logs_without_stdout(monkeypatch, caplog, capsys):
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+    monkeypatch.setitem(sys.modules, "torch", _fake_torch(hip=None))
+    monkeypatch.setattr(transcriber_mod, "ct2_cuda_device_count", lambda: 0)
+    monkeypatch.setattr(transcriber_mod, "WhisperModel", _RecordingWhisperModel)
+
+    with caplog.at_level(logging.WARNING, logger="stt_daemon.transcriber"):
+        transcriber = Transcriber(
+            "small", device="cuda", compute_type="float16", language=None
+        )
+
+    assert capsys.readouterr().out == ""
+    assert transcriber.device == "cpu"
+    assert transcriber.compute_type == "int8"
+    assert "CUDA requested but not available" in caplog.text
+
+
+@pytest.mark.parametrize(
+    ("compute_type", "expected"),
+    [
+        ("float16", "int8"),
+        ("int8_float16", "int8"),
+        ("bfloat16", "int8"),
+        ("int8_bfloat16", "int8"),
+        ("int8", "int8"),
+        ("int8_float32", "int8_float32"),
+        ("float32", "float32"),
+    ],
+)
+def test_cpu_safe_compute_type(compute_type, expected):
+    assert accel.cpu_safe_compute_type(compute_type) == expected
+
+
+def test_transcriber_downgrades_gpu_compute_type_when_no_device(monkeypatch, caplog):
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: True)
+    monkeypatch.setattr(transcriber_mod, "ct2_cuda_device_count", lambda: 0)
+    monkeypatch.setattr(transcriber_mod, "WhisperModel", _RecordingWhisperModel)
+
+    with caplog.at_level(logging.WARNING, logger="stt_daemon.transcriber"):
+        transcriber = Transcriber(
+            "small", device="auto", compute_type="float16", language=None
+        )
+
+    assert _RecordingWhisperModel.last is not None
+    assert _RecordingWhisperModel.last.device_arg == "cpu"
+    assert _RecordingWhisperModel.last.compute_type_arg == "int8"
+    assert transcriber.device == "cpu"
+    assert transcriber.compute_type == "int8"
+    assert "device=auto resolved to CPU" in caplog.text
+    assert "needs a GPU" in caplog.text
+
+
+def test_transcriber_downgrades_gpu_compute_type_on_explicit_cpu(monkeypatch, caplog):
+    monkeypatch.delenv(accel.CT2_ALLOCATOR_ENV, raising=False)
+    monkeypatch.setattr(accel, "is_rocm_host", lambda: False)
+    monkeypatch.setattr(transcriber_mod, "ct2_cuda_device_count", lambda: 1)
+    monkeypatch.setattr(transcriber_mod, "WhisperModel", _RecordingWhisperModel)
+
+    with caplog.at_level(logging.WARNING, logger="stt_daemon.transcriber"):
+        Transcriber("small", device="cpu", compute_type="float16", language=None)
+
+    assert _RecordingWhisperModel.last is not None
+    assert _RecordingWhisperModel.last.compute_type_arg == "int8"
+    assert "needs a GPU" in caplog.text

+ 38 - 26
uv.lock

@@ -329,7 +329,7 @@ wheels = [
 
 [[package]]
 name = "ctranslate2"
-version = "4.8.2"
+version = "4.8.4"
 source = { registry = "https://pypi.org/simple" }
 dependencies = [
     { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
@@ -337,31 +337,41 @@ dependencies = [
     { name = "pyyaml" },
 ]
 wheels = [
-    { url = "https://files.pythonhosted.org/packages/43/17/f22fbbb0723891704af803e1e7af5541bb46013adb347f870cbd8c4834d3/ctranslate2-4.8.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cb2a5b4c206b3bd1f7f4b44d07e86e42050af6753053cdc86444f8d0ead9d6d8", size = 1271641, upload-time = "2026-08-31T19:37:07.757Z" },
-    { url = "https://files.pythonhosted.org/packages/b2/af/cfe767012a0809c15155ecae3125ca66a16aeee4669bae146220cc631a76/ctranslate2-4.8.2-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:8221875b09ef982e3579a5165f8c9fcfb3d5de0f710af228eca2f73d69635ace", size = 11929414, upload-time = "2026-08-31T19:37:09.435Z" },
-    { url = "https://files.pythonhosted.org/packages/10/2e/2d08b1219303af7256aeb78b98ee847ffb229c5fe4815e6d3a1b6483846a/ctranslate2-4.8.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3823c9883c2c410a76b2f19feda9da628a3c112cedfd912db815e0055c8235e2", size = 16720002, upload-time = "2026-08-31T19:37:11.957Z" },
-    { url = "https://files.pythonhosted.org/packages/f3/3d/75c029ffb484957f1d2be0f9c3b9b1474955bb16c22df85bcba50c48df11/ctranslate2-4.8.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a24e0a95151b970941867fec94c983df978630f1996242a587c072d455607d28", size = 39378274, upload-time = "2026-08-31T19:37:14.898Z" },
-    { url = "https://files.pythonhosted.org/packages/7e/86/21926da682103a20d4f4833b389131b801c71e6b8d6047be5f0a28fe673a/ctranslate2-4.8.2-cp311-cp311-win_amd64.whl", hash = "sha256:995938fcd24a1174a7abf9765e7fa216b5b91a1d8e8c4c8f383c7a186e8bab2e", size = 19220522, upload-time = "2026-08-31T19:37:17.845Z" },
-    { url = "https://files.pythonhosted.org/packages/12/97/9c63a51a8c8e13e95ec2aec639460fb0f271b0421e1a365b8aad440385c4/ctranslate2-4.8.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fedda669421a57f8164568ee86ea265727016988d310fcc2e24ca30e9ec457cd", size = 1271613, upload-time = "2026-08-31T19:37:19.958Z" },
-    { url = "https://files.pythonhosted.org/packages/ca/27/e419389fb6040a8170bb30c3dcd50c29b70638852a264acc5bf804bf8800/ctranslate2-4.8.2-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5385f15493e7b6f41377da83d0d5afeb8e0f36188260722f3800c3c7121455af", size = 11931717, upload-time = "2026-08-31T19:37:21.897Z" },
-    { url = "https://files.pythonhosted.org/packages/53/46/bfa42114fd583b0ef30b67113486ab72d3ff466e60a824739257bcc533bd/ctranslate2-4.8.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:781f835da7fdc4adcc38f9d674dbe9b37eaf3c9aee3b2bc17d684342ff59d549", size = 16894628, upload-time = "2026-08-31T19:37:24.328Z" },
-    { url = "https://files.pythonhosted.org/packages/01/23/d72e70cac2b7c5c832a629c380235b53b20fac8c0921856bcf625b08ba44/ctranslate2-4.8.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:554a0b2abf098a11c66ef7930c0a05b3de683a455e8ea3f2e6ab6966aaabc136", size = 39555908, upload-time = "2026-08-31T19:37:27.948Z" },
-    { url = "https://files.pythonhosted.org/packages/4e/23/e3b5322ff7368fcbed181ea4c209149416e7940b5b04971d5ee4084afe1a/ctranslate2-4.8.2-cp312-cp312-win_amd64.whl", hash = "sha256:d94421d565d0de61c032998f737a18942b0f2bef40c0424b1846ec6f67300105", size = 19222069, upload-time = "2026-08-31T19:37:31.531Z" },
-    { url = "https://files.pythonhosted.org/packages/2c/00/3f5d12d94daa22ddf51de0252313476c0f8fca8b1c7776592953764a8b85/ctranslate2-4.8.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a40b190248389e1adb38ff90407937131c6bb9d7f9771f29ca1dca01946ff252", size = 1271572, upload-time = "2026-08-31T19:37:33.476Z" },
-    { url = "https://files.pythonhosted.org/packages/b4/b5/fde838502472462f2c8fd9e8fe17e734854bbe1047452c28d4fbe3eab7b2/ctranslate2-4.8.2-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:2778eaf89340062feb0d2d3dd35bedb14644673ccbce8934782dc8d093b2cab9", size = 11931591, upload-time = "2026-08-31T19:37:35.016Z" },
-    { url = "https://files.pythonhosted.org/packages/a6/c7/cf22407330c90c1f3ef642ece11ff6bfe686509e995b95545951edf29117/ctranslate2-4.8.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:249b35f575adf8dd11e6ba43b212d5cbfc845d1140abc610c2872eef1ffb3481", size = 16895415, upload-time = "2026-08-31T19:37:37.342Z" },
-    { url = "https://files.pythonhosted.org/packages/89/8b/051962470b8e9df1a43f0c9c7a03ce1c8a279db76be4e6a6fcb5171d1d29/ctranslate2-4.8.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:633f19d1ff8d053a8b179ca742b242002ac7882a2ab44e319c6efcde27d586a4", size = 39555517, upload-time = "2026-08-31T19:37:40.87Z" },
-    { url = "https://files.pythonhosted.org/packages/c2/fc/a9e9e0ce1c0a29bc4c17bf56ccb4274293c0bbb2b8aae561727d82fcb0ca/ctranslate2-4.8.2-cp313-cp313-win_amd64.whl", hash = "sha256:399c20a7336b6358f69ce3c615e090eabc08f29735a079fd1ac1e464119e0861", size = 19222061, upload-time = "2026-08-31T19:37:43.754Z" },
-    { url = "https://files.pythonhosted.org/packages/72/b4/28111e86927b2490388d8e0b79ec83db38f2a81bf452318f24563b42e205/ctranslate2-4.8.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e32ae5e625a8999749745de43c2de5c12ceebb6d0d9cc9a3a956e8912bd68edb", size = 1272406, upload-time = "2026-08-31T19:37:45.671Z" },
-    { url = "https://files.pythonhosted.org/packages/03/07/444fccd37ba9e3d84dcb985fc174c82636e7a8c69b47bb0c1bf566045d37/ctranslate2-4.8.2-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:980f13e1d41987908af945861081158fbcb2983f4f9399514adc8c87378b5587", size = 11932080, upload-time = "2026-08-31T19:37:47.24Z" },
-    { url = "https://files.pythonhosted.org/packages/f9/c5/c8d91fa1848f9bc09e943337b9c7ebd20d77f1344f5d595d10979f5e59d4/ctranslate2-4.8.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e866ccf5f5668f7302c39b535d19d7e5811f3f00d6246faf6ed6fa070fa1af3", size = 16884555, upload-time = "2026-08-31T19:37:50.337Z" },
-    { url = "https://files.pythonhosted.org/packages/14/0c/de0305052c2bb35979c4a6c668c7aa6f139c10bc0939833450b462cef829/ctranslate2-4.8.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:573dcf5d96034dd9bcf7ee0db1657af490a52292ffc77dfb4b104da1e93322ad", size = 39521307, upload-time = "2026-08-31T19:37:53.623Z" },
-    { url = "https://files.pythonhosted.org/packages/f2/86/03386a60b634a34c820e00f82bec808d38841cc2c007ec6b797892a9a5b5/ctranslate2-4.8.2-cp314-cp314-win_amd64.whl", hash = "sha256:00bc9f44172d05bd2becefe757d2397ab3292fad1695169f9ebb9c16eb497ce2", size = 19478465, upload-time = "2026-08-31T19:37:56.488Z" },
-    { url = "https://files.pythonhosted.org/packages/33/46/f25a66e905d3ce13aba221e6e7d63e24acd2539e48549675e3e0527f9ffe/ctranslate2-4.8.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:58b1a78d050a7f281907b8acfe6f0cdbbf2feaf7464f38eb05b846f83fbaa297", size = 1295017, upload-time = "2026-08-31T19:37:58.379Z" },
-    { url = "https://files.pythonhosted.org/packages/28/c2/0fc88102f448aee8fa704fec13e568f716df11aa701e901b2e401aa5d8ab/ctranslate2-4.8.2-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:70e2388d31dfb59968e6e011d01a6fc6bf802c0e1ad920a8d2a413f1c9da02ae", size = 11953132, upload-time = "2026-08-31T19:38:00.199Z" },
-    { url = "https://files.pythonhosted.org/packages/62/23/f859ee8af1c6366795d59979a7e941a397823cc38585f5731c34a66f1048/ctranslate2-4.8.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:32b9d0e984aa17a0da9b37bb7e4dcc638e898a2c32de08d5a4db53f15f640faa", size = 16872477, upload-time = "2026-08-31T19:38:02.641Z" },
-    { url = "https://files.pythonhosted.org/packages/63/87/b964f427fcdfad983977859386007e1367eb340f747f63b6c0001cf78905/ctranslate2-4.8.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd9ab8b521230e8e962a58362b1ccde582806e781af8c1b9d7b244f3024468b1", size = 39491894, upload-time = "2026-08-31T19:38:06.365Z" },
-    { url = "https://files.pythonhosted.org/packages/6a/39/9d316f00f184cea15e807a977df5bc76fc8f27f81246015f12083f42cd1c/ctranslate2-4.8.2-cp314-cp314t-win_amd64.whl", hash = "sha256:f6f0b576c247984d3fc299a372ccc9319b668d6d25b0539f3c861beba15d0504", size = 19500896, upload-time = "2026-08-31T19:38:09.111Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/01/4bdbbb7f564725629dfa638c4492997e12697822d5f5855a61705d116bb3/ctranslate2-4.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7b6cd9e832e47c00d3da14976b1621b065d8680a7d27a95a009af4a41e169ac", size = 1489306, upload-time = "2026-10-25T19:04:02.114Z" },
+    { url = "https://files.pythonhosted.org/packages/da/a7/c9cd5ab778b2f1eed8c4552e94c236e4e26a3742ffd54eb8ec54880fec8f/ctranslate2-4.8.4-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:2c366cb89ffcd99f76e44dfbb199979d450a9c698dae5eefd65e94a7ef25443c", size = 12167392, upload-time = "2026-10-25T19:04:03.775Z" },
+    { url = "https://files.pythonhosted.org/packages/b8/f9/e0250f31eadfbb70e467b2c33867399665ab783ecc3fff18412595b1693f/ctranslate2-4.8.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bd6ed9d10b46b9bc2ee801ea9624acf28bd53ffddfd5e92d23b2340dbc4d6474", size = 18793231, upload-time = "2026-10-25T19:04:06.692Z" },
+    { url = "https://files.pythonhosted.org/packages/d5/fb/c20dd76f7636bdef710e2f6e8419db3a07687cec65e78228ff3624875afe/ctranslate2-4.8.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1037b3f51d18f1546120686670d71fcdaf85a0500fffccc56d8439ded5d284e5", size = 42428932, upload-time = "2026-10-25T19:04:10.532Z" },
+    { url = "https://files.pythonhosted.org/packages/45/a0/7f9744949aac9a143a1245511fb0ce5f62434aed9b1ca026cb72d9ff6e5f/ctranslate2-4.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:01bfca5d6e1736e38602511066a437677470c690e1458fa7c72ff322a3cfc4f9", size = 20369372, upload-time = "2026-10-25T19:04:13.542Z" },
+    { url = "https://files.pythonhosted.org/packages/45/18/68692f8cf2684690cc6f2b407fa6d5909895cac37aa9d1df1bc5849017f9/ctranslate2-4.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:36ebf8d2f5b7af176cef7869cd98a1c722ee01c177168fda5c9a045fc930be67", size = 1490254, upload-time = "2026-10-25T19:04:15.694Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/2f/7495643e0d5f530f927bbaf9daecb180606fe484d6afc056fcb773974344/ctranslate2-4.8.4-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:1094d0128657903677b3be3aab9186bf28ac8d89a4676fb590bcabf3c7605971", size = 12172517, upload-time = "2026-10-25T19:04:17.411Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/0f/f7aad2355dbb4a0115d9265f0ab04feddfd506388d19bfe7e0387fd7c5e1/ctranslate2-4.8.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a102683e7dc5d0cd0c31de5771ce4c27fffa837721470e7aae64ebffa64f3206", size = 18976989, upload-time = "2026-10-25T19:04:19.951Z" },
+    { url = "https://files.pythonhosted.org/packages/39/42/ab136f76cf379ed97d3f95563ad227fdd5cdbbeb20eaa797b0962be62b1a/ctranslate2-4.8.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d1830d12c84b2240bf51893ee6bfe24a671dccfd232b05a74ef197d27fb164a", size = 42669573, upload-time = "2026-10-25T19:04:23.931Z" },
+    { url = "https://files.pythonhosted.org/packages/af/25/cca7f6807d37cbfd9eebed8bbf136f5214c31d7d66de267ad05b676e9deb/ctranslate2-4.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:056db4b4c2a31b7811d453155a3d6ab316ddb1dc104f413beee7bce26bcbf92d", size = 20371102, upload-time = "2026-10-25T19:04:26.986Z" },
+    { url = "https://files.pythonhosted.org/packages/56/83/f96841f2123ee983c41a69477ca00f171d75e6434a4c40c0e3decbe457bb/ctranslate2-4.8.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:183a21b4c0da1846f8c3faac0267c497d7bdc8dd43350e23fa4f584b4ba2d4ba", size = 1490195, upload-time = "2026-10-25T19:04:29.168Z" },
+    { url = "https://files.pythonhosted.org/packages/40/ef/570715e8b607df28e129e913a496f41e6efa2665b85e3536f54e87b31d1e/ctranslate2-4.8.4-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:a547c5970c17c3259a59e731e5479579463b2d3a6235862a99a46654437faeb9", size = 12172432, upload-time = "2026-10-25T19:04:31.073Z" },
+    { url = "https://files.pythonhosted.org/packages/db/5a/29467590dbc2fe339e159da946f9192f231cfd753d36dd30095de88d6578/ctranslate2-4.8.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f4ed771f82c4078113d811c5f9ad0a4597bb79f54784adb58fd50d69b04cb7a5", size = 18959426, upload-time = "2026-10-25T19:04:33.746Z" },
+    { url = "https://files.pythonhosted.org/packages/17/0b/430c7f43bc06cbdb5f9f66dac1ce13c1d713e03e729b295856a51ac33043/ctranslate2-4.8.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:271e77faf49ff9fa117b490b9008ac0b294744c19dabc93087e202299c1a8e27", size = 42612303, upload-time = "2026-10-25T19:04:37.457Z" },
+    { url = "https://files.pythonhosted.org/packages/37/ae/4fbe636b36fd4fd4266e309d050548c1ae0a23c88ef73ef89c6485db99d1/ctranslate2-4.8.4-cp313-cp313-win_amd64.whl", hash = "sha256:d9523961651ac0f51e4e62d03307609ff93d828b32ae92dc8b14a7e55243f6f2", size = 20371182, upload-time = "2026-10-25T19:04:40.548Z" },
+    { url = "https://files.pythonhosted.org/packages/7a/3e/3f133db61ec4483b67ca264b63ae5fef9d60c0c5d6f253fad3f9bbf9e850/ctranslate2-4.8.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:87145e4ebdabd19c7bc8eb1f96a30b2df2b6923e6bdea8c97c637c872962f51a", size = 1490930, upload-time = "2026-10-25T19:04:43.285Z" },
+    { url = "https://files.pythonhosted.org/packages/3c/fb/b38afe110dc1af43a45434e4c37564371e10ce9e91a3b200dda4494cc851/ctranslate2-4.8.4-cp314-cp314-macosx_11_0_x86_64.whl", hash = "sha256:40d49ce50fc38432eae920c36a13fb943fc73363934ff699986eadaf1a9e178b", size = 12172898, upload-time = "2026-10-25T19:04:45.409Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/e1/2f4b85f47567b934fb3e38da9f76451fd243f5111daf3dcef7b9211dfb7d/ctranslate2-4.8.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec9639dd36511eecf23bbc99a61a8fe77f16e2169e8dd35be6502ae3b2147a20", size = 18934359, upload-time = "2026-10-25T19:04:48.674Z" },
+    { url = "https://files.pythonhosted.org/packages/0d/f4/d14d4ad8d1eac0e49aafcc11ff9ec83ac5758ae23acc526f15b73c980c19/ctranslate2-4.8.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e838fb78e8db9daee96187a08c3d0839bf45bd4853afda839f3b4b4979336ed6", size = 42590297, upload-time = "2026-10-25T19:04:52.638Z" },
+    { url = "https://files.pythonhosted.org/packages/50/98/6bd005eb63150a8d029aa06076180bc3452243b56da592c770128ac6f47b/ctranslate2-4.8.4-cp314-cp314-win_amd64.whl", hash = "sha256:4b322215f3b8eaab1db45b9e6aebfb8ddfe1336651e15a569c90826bfdb4b136", size = 20638792, upload-time = "2026-10-25T19:04:55.966Z" },
+    { url = "https://files.pythonhosted.org/packages/89/7b/51b68b23870e9e09c3dda27e629b78dbbc259750036474bea8e38171f721/ctranslate2-4.8.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9fc200ff85351d6e6f7403c43857086df8ebc754bf243a77079fee7c2156ada5", size = 1517691, upload-time = "2026-10-25T19:04:58.538Z" },
+    { url = "https://files.pythonhosted.org/packages/45/9d/83f3f40d0e07878f62eb3b633ca186fe163b3c4fbdb960ab7bb66064a625/ctranslate2-4.8.4-cp314-cp314t-macosx_11_0_x86_64.whl", hash = "sha256:1cf96ce8f625166ef67358b42c798271b307bb0b4a1d39a9965ff848f17f9d74", size = 12199614, upload-time = "2026-10-25T19:05:00.307Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/e3/a1a4b5a2b66acda963f08a6e73a4866ad79fa7bd079569be881da8d08f25/ctranslate2-4.8.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:230b5216b71f5502d6ae0d9151c646aa69de7817b2e424ee67d6a87a9aeb19e1", size = 19315986, upload-time = "2026-10-25T19:05:03.08Z" },
+    { url = "https://files.pythonhosted.org/packages/78/69/8750a4bd23c85a41d14686de0fadb101d5241a2a79e0c5a36d56d04c69ba/ctranslate2-4.8.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bbf71062d93031ad709a20588a6d2cb624f66253c53e8509c8f9410078d53bd9", size = 42942312, upload-time = "2026-10-25T19:05:07.228Z" },
+    { url = "https://files.pythonhosted.org/packages/22/3e/94c4fb70d2844b5b396fa29e04139013292b8129feab427c6241953a1a6f/ctranslate2-4.8.4-cp314-cp314t-win_amd64.whl", hash = "sha256:81397be8ef6c7966ebe5b549244114e543f19219976d7e1437fdebb624f296c0", size = 20665797, upload-time = "2026-10-25T19:05:10.444Z" },
+    { url = "https://files.pythonhosted.org/packages/e8/de/7dd4ad23e7fa5c0ebb3be622d94283a73885847f886c7803e5e683617548/ctranslate2-4.8.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ed6d6e53e9414f702868f8c9543da8b526b8d1703dd0c1136c99d01c8e6b0614", size = 1491298, upload-time = "2026-10-25T19:05:12.626Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/4a/a37f99662f23132282540b33c26080dc498d66a3dd63418026d6bf143639/ctranslate2-4.8.4-cp315-cp315-macosx_11_0_x86_64.whl", hash = "sha256:18286b049aa9afb1df301bf627eb8dcbe4e9069810e7e3e4ef069e55a68b5934", size = 12172901, upload-time = "2026-10-25T19:05:14.422Z" },
+    { url = "https://files.pythonhosted.org/packages/a8/7d/56ef3c91f58c1ad99daaf1b4ff798109e6a73ae5784549e0bcc3efe37b6f/ctranslate2-4.8.4-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ac23582ee919235fd6a73ab383eb7cb97b544dc7c70fcc445dc22de7d70dc90", size = 18944558, upload-time = "2026-10-25T19:05:17.362Z" },
+    { url = "https://files.pythonhosted.org/packages/58/cc/07a6c500aa65910691eb345fb4fca463906168cd75c6dde8735f02f01746/ctranslate2-4.8.4-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b6139f57b316420469d1400968d40185ff7b99123d45a88ab3112d364809fffb", size = 42613272, upload-time = "2026-10-25T19:05:21.321Z" },
+    { url = "https://files.pythonhosted.org/packages/d4/93/c3f255c6a7e7aded6c3fcabbb383d5b9277f05ff62d960dd9a99dae698f1/ctranslate2-4.8.4-cp315-cp315-win_amd64.whl", hash = "sha256:a6c28ac653a3598d3a978e9c9c629c3a4f30072b0d0473be2880d3d82599e11a", size = 20638838, upload-time = "2026-10-25T19:05:24.766Z" },
+    { url = "https://files.pythonhosted.org/packages/ba/9f/890c9c520135642c5c338c0687872f41e65e25dd1d58a217d514258c9211/ctranslate2-4.8.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:8eb7a167ff37ddc2c0237064fd25101fe00775a7deaf1f094be9c7f88694d54c", size = 1518152, upload-time = "2026-10-25T19:05:27.037Z" },
+    { url = "https://files.pythonhosted.org/packages/ec/80/08a06ade4d67b63384189c280edfd00be9a5ec3b9f6c0d263d322b7c12e5/ctranslate2-4.8.4-cp315-cp315t-macosx_11_0_x86_64.whl", hash = "sha256:63a715c34b5e3c462e5680ea32ea12bad30ada8db1a6d99fc8e0e541d6117542", size = 12199353, upload-time = "2026-10-25T19:05:29.26Z" },
+    { url = "https://files.pythonhosted.org/packages/a7/1d/b44cd0038a265768ba649bd45359e6029be670fd6f0135a5d07637d38182/ctranslate2-4.8.4-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:82caf13dce0563d96a92e0bcfe541f3ff73875b4d3d9335e8dfc46a66d0e2a51", size = 19368566, upload-time = "2026-10-25T19:05:32.297Z" },
+    { url = "https://files.pythonhosted.org/packages/01/76/81a2e86d63183c597ef977c03d19166bab0857c1a92bdcfe394770bbd274/ctranslate2-4.8.4-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8849e5dfd27f3e5ad781d327f477434badfcb3deead4c4758ceeb621aed930a7", size = 43008255, upload-time = "2026-10-25T19:05:36.566Z" },
+    { url = "https://files.pythonhosted.org/packages/ae/f7/d3ca639be986018bc1ee06e8c07652c44fc7c65b70022f161a7adce2d014/ctranslate2-4.8.4-cp315-cp315t-win_amd64.whl", hash = "sha256:b8f5ea44f93342cb460642f966bb87bf79d3fdcded6db049001cca28e82c0be8", size = 20666116, upload-time = "2026-10-25T19:05:39.895Z" },
 ]
 
 [[package]]
@@ -1596,6 +1606,7 @@ name = "stt-daemon"
 version = "0.1.0"
 source = { editable = "." }
 dependencies = [
+    { name = "ctranslate2" },
     { name = "faster-whisper" },
     { name = "numpy", version = "2.4.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" },
     { name = "numpy", version = "2.5.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" },
@@ -1625,6 +1636,7 @@ dev = [
 
 [package.metadata]
 requires-dist = [
+    { name = "ctranslate2", specifier = ">=4.8.4" },
     { name = "faster-whisper", specifier = ">=1.1.0" },
     { name = "laya", marker = "extra == 'triage'", git = "https://github.com/NandhaKishorM/laya.git?rev=970dc8c5f63d7b886a68409493f37d569424f933" },
     { name = "numpy", specifier = ">=1.26" },