File size: 2,511 Bytes
0fe3056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
"""Local ASR transcription with faster-whisper, run on the downloaded video file.

Used when the pipeline downloads the source video (via the RapidAPI fast-downloader):
faster-whisper reads the media directly (PyAV decodes the audio track), so no separate
audio extraction is needed. Returns ``[{start, end, text}]`` segments — the same shape the
caption-based path produces — so the rest of the pipeline is unchanged.

Model size / device are configurable via env: ``WHISPER_MODEL`` (default ``base``),
``WHISPER_DEVICE`` (default ``cpu``), ``WHISPER_COMPUTE`` (default ``int8`` — fastest on
CPU). ``base``/``int8`` is a sensible CPU default; use ``small``/``medium`` (or a GPU) for
higher accuracy.
"""
from __future__ import annotations

import os
from functools import lru_cache


class ASRError(RuntimeError):
    """Raised when local transcription fails."""


@lru_cache(maxsize=1)
def _model():
    from faster_whisper import WhisperModel

    size = os.environ.get("WHISPER_MODEL", "base")
    device = os.environ.get("WHISPER_DEVICE", "cpu")
    compute = os.environ.get("WHISPER_COMPUTE", "int8")
    return WhisperModel(size, device=device, compute_type=compute)


def transcribe_file(media_path: str, language: str | None = "en",
                    progress=None) -> list[dict]:
    """Transcribe ``media_path`` (audio or video) into ``[{start, end, text}]``.

    ``language`` pins the language (faster + avoids misdetection); pass None to
    auto-detect.
    """
    if not os.path.exists(media_path):
        raise ASRError(f"media file not found: {media_path}")
    try:
        model = _model()
        # No vad_filter: it depends on onnxruntime (Silero VAD) that isn't always
        # present and, when broken, silently drops every segment. beam_size=1 for speed.
        segments, info = model.transcribe(media_path, language=language, beam_size=1)
    except Exception as exc:
        raise ASRError(f"faster-whisper failed: {type(exc).__name__}: {exc}") from exc

    total = float(getattr(info, "duration", 0) or 0)
    out: list[dict] = []
    for s in segments:  # generator — transcription happens as we iterate
        text = (s.text or "").strip()
        if text:
            out.append({"start": float(s.start), "end": float(s.end), "text": text})
        if progress and total:
            progress(min(1.0, (s.end or 0) / total), desc="Transcribing (faster-whisper)")
    if not out:
        raise ASRError("faster-whisper produced no speech segments.")
    return out