| import json |
| import os |
| from functools import lru_cache |
| from pathlib import Path |
| from typing import Any, Callable, Optional |
|
|
| from faster_whisper import WhisperModel |
|
|
| from . import config |
|
|
|
|
| def cleanup_outputs() -> None: |
| for child in config.OUTPUT_DIR.iterdir(): |
| if child.is_file(): |
| child.unlink() |
|
|
|
|
| def _detect_device() -> tuple[str, str]: |
| """Return (device, compute_type). Prefers CUDA when available.""" |
| try: |
| import ctranslate2 |
| if ctranslate2.get_cuda_device_count() > 0: |
| return "cuda", "float16" |
| except Exception: |
| pass |
| return "cpu", "int8" |
|
|
|
|
| |
| DEVICE, COMPUTE_TYPE = _detect_device() |
| CPU_THREADS = max(1, min(os.cpu_count() or 4, 8)) |
|
|
|
|
| @lru_cache(maxsize=8) |
| def get_model(model_size: str) -> WhisperModel: |
| kwargs: dict[str, Any] = {"device": DEVICE, "compute_type": COMPUTE_TYPE} |
| if DEVICE == "cpu": |
| kwargs["cpu_threads"] = CPU_THREADS |
| kwargs["num_workers"] = 2 |
| return WhisperModel(model_size, **kwargs) |
|
|
|
|
| def device_info() -> dict[str, str]: |
| return {"device": DEVICE, "compute_type": COMPUTE_TYPE} |
|
|
|
|
| def _srt_time(s: float) -> str: |
| h, rem = divmod(int(s), 3600) |
| m, sec = divmod(rem, 60) |
| ms = int((s % 1) * 1000) |
| return f"{h:02d}:{m:02d}:{sec:02d},{ms:03d}" |
|
|
|
|
| def _vtt_time(s: float) -> str: |
| h, rem = divmod(int(s), 3600) |
| m, sec = divmod(rem, 60) |
| ms = int((s % 1) * 1000) |
| return f"{h:02d}:{m:02d}:{sec:02d}.{ms:03d}" |
|
|
|
|
| def format_segments(segments: list[dict], fmt: str) -> str: |
| if fmt == "txt": |
| return "\n".join(s["text"] for s in segments) |
|
|
| if fmt == "srt": |
| lines: list[str] = [] |
| for i, s in enumerate(segments, 1): |
| lines += [str(i), f"{_srt_time(s['start'])} --> {_srt_time(s['end'])}", s["text"], ""] |
| return "\n".join(lines) |
|
|
| if fmt == "vtt": |
| lines = ["WEBVTT", ""] |
| for s in segments: |
| lines += [f"{_vtt_time(s['start'])} --> {_vtt_time(s['end'])}", s["text"], ""] |
| return "\n".join(lines) |
|
|
| if fmt == "json": |
| return json.dumps({"segments": segments}, indent=2, ensure_ascii=False) |
|
|
| if fmt == "tsv": |
| lines = ["start\tend\ttext"] |
| lines += [f"{int(s['start'] * 1000)}\t{int(s['end'] * 1000)}\t{s['text']}" for s in segments] |
| return "\n".join(lines) |
|
|
| return "\n".join(s["text"] for s in segments) |
|
|
|
|
| def transcribe_file( |
| audio_path: Path, |
| model_size: str, |
| language: Optional[str] = None, |
| task: str = "transcribe", |
| word_timestamps: bool = False, |
| no_condition: bool = False, |
| fmt: str = "srt", |
| progress_callback: Optional[Callable[[int, str], None]] = None, |
| ) -> dict[str, Any]: |
| if progress_callback: |
| progress_callback(5, f"Loading model ({DEVICE.upper()})…") |
|
|
| model = get_model(model_size) |
|
|
| if progress_callback: |
| progress_callback(12, "Analysing audio…") |
|
|
| segments_iter, info = model.transcribe( |
| str(audio_path), |
| language=language, |
| task=task, |
| word_timestamps=word_timestamps, |
| condition_on_previous_text=not no_condition, |
| vad_filter=True, |
| |
| beam_size=1, |
| best_of=1, |
| temperature=0.0, |
| ) |
|
|
| result_segments: list[dict] = [] |
| total = max(info.duration or 1.0, 1.0) |
|
|
| for segment in segments_iter: |
| seg: dict[str, Any] = { |
| "start": round(segment.start, 3), |
| "end": round(segment.end, 3), |
| "text": segment.text.strip(), |
| } |
| if word_timestamps and segment.words: |
| seg["words"] = [ |
| {"start": round(w.start, 3), "end": round(w.end, 3), "word": w.word} |
| for w in segment.words |
| ] |
| result_segments.append(seg) |
|
|
| if progress_callback: |
| pct = min(95, 15 + int((segment.end / total) * 80)) |
| progress_callback(pct, f"Transcribing… {int(segment.end)}s / {int(total)}s") |
|
|
| formatted = format_segments(result_segments, fmt) |
| full_text = " ".join(s["text"] for s in result_segments).strip() |
|
|
| return { |
| "text": full_text, |
| "segments": result_segments, |
| "language": info.language, |
| "language_probability": info.language_probability, |
| "duration": round(info.duration, 2), |
| "format": fmt, |
| "model_size": model_size, |
| "task": task, |
| "formatted": formatted, |
| } |
|
|