101 lines
2.9 KiB
Python
101 lines
2.9 KiB
Python
from pathlib import Path
|
|
import os
|
|
import sys
|
|
import threading
|
|
import logging
|
|
import wave
|
|
import json
|
|
import subprocess
|
|
import shutil
|
|
|
|
try:
|
|
from vosk import Model, KaldiRecognizer
|
|
except Exception:
|
|
Model = None
|
|
KaldiRecognizer = None
|
|
|
|
|
|
class FFmpegConverter:
|
|
"""Utility to convert audio files using ffmpeg."""
|
|
|
|
@staticmethod
|
|
def convert_mp3_to_wav(src: Path, dest: Path) -> None:
|
|
cmd = [
|
|
"ffmpeg",
|
|
"-y",
|
|
"-i",
|
|
str(src),
|
|
"-ac",
|
|
"1",
|
|
"-ar",
|
|
"16000",
|
|
"-vn",
|
|
"-f",
|
|
"wav",
|
|
str(dest),
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
|
|
|
|
class VoskModelManager:
|
|
"""Loads a Vosk model and provides transcription methods.
|
|
|
|
Loading captures model stderr into Python logs to show progress.
|
|
"""
|
|
|
|
def __init__(self, model_path: Path):
|
|
self.logger = logging.getLogger("vosk.model_manager")
|
|
if Model is None or KaldiRecognizer is None:
|
|
raise RuntimeError("vosk is not installed in runtime")
|
|
self.model = self._load_model_with_progress(model_path)
|
|
|
|
def _load_model_with_progress(self, path: Path):
|
|
logger = logging.getLogger("vosk.loader")
|
|
r_fd, w_fd = os.pipe()
|
|
saved_stderr_fd = os.dup(sys.stderr.fileno())
|
|
os.dup2(w_fd, sys.stderr.fileno())
|
|
|
|
def _reader(fd):
|
|
with os.fdopen(fd, "rb") as fh:
|
|
for raw in iter(fh.readline, b""):
|
|
try:
|
|
logger.info(raw.decode(errors="ignore").rstrip())
|
|
except Exception:
|
|
pass
|
|
|
|
reader_thread = threading.Thread(target=_reader, args=(r_fd,), daemon=True)
|
|
reader_thread.start()
|
|
|
|
try:
|
|
model = Model(str(path))
|
|
finally:
|
|
try:
|
|
os.dup2(saved_stderr_fd, sys.stderr.fileno())
|
|
except Exception:
|
|
pass
|
|
try:
|
|
os.close(saved_stderr_fd)
|
|
except Exception:
|
|
pass
|
|
reader_thread.join(timeout=2)
|
|
|
|
return model
|
|
|
|
def transcribe_wav(self, wav_path: Path) -> str:
|
|
if not wav_path.exists():
|
|
raise FileNotFoundError(wav_path)
|
|
try:
|
|
with wave.open(str(wav_path), "rb") as wf:
|
|
if wf.getnchannels() != 1:
|
|
raise ValueError("Audio must be mono (1 channel)")
|
|
recognizer = KaldiRecognizer(self.model, wf.getframerate())
|
|
while True:
|
|
data = wf.readframes(4000)
|
|
if not data:
|
|
break
|
|
recognizer.AcceptWaveform(data)
|
|
result = json.loads(recognizer.FinalResult())
|
|
return result.get("text", "")
|
|
except wave.Error as e:
|
|
raise ValueError(f"Invalid WAV file: {e}")
|