|
|
@@ -0,0 +1,71 @@
|
|
|
+"""faster-whisper 本地 STT 引擎封装"""
|
|
|
+import os
|
|
|
+import logging
|
|
|
+import numpy as np
|
|
|
+import av
|
|
|
+from faster_whisper import WhisperModel
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+_model = None
|
|
|
+_loaded = False
|
|
|
+
|
|
|
+
|
|
|
+def init_model(model_size: str = "base", device: str = "cpu", compute_type: str = "int8"):
|
|
|
+ """初始化 faster-whisper 模型(启动时调用)"""
|
|
|
+ global _model, _loaded
|
|
|
+ if _loaded:
|
|
|
+ return
|
|
|
+ os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
|
|
|
+ logger.info("加载 faster-whisper 模型 %s (device=%s, compute=%s)...", model_size, device, compute_type)
|
|
|
+ _model = WhisperModel(model_size, device=device, compute_type=compute_type)
|
|
|
+ _loaded = True
|
|
|
+ logger.info("faster-whisper 模型加载完成")
|
|
|
+
|
|
|
+
|
|
|
+def transcribe_audio(file_path: str, language: str = "zh") -> dict:
|
|
|
+ """转写音频文件,返回 {text, language, language_probability, duration}"""
|
|
|
+ if not _model:
|
|
|
+ raise RuntimeError("faster-whisper 模型未初始化")
|
|
|
+
|
|
|
+ duration_sec = 0.0
|
|
|
+ try:
|
|
|
+ with av.open(file_path) as container:
|
|
|
+ stream = container.streams.audio[0]
|
|
|
+ audio_chunks = []
|
|
|
+ sample_cnt = 0
|
|
|
+ for frame in container.decode(audio=0):
|
|
|
+ arr = frame.to_ndarray()
|
|
|
+ sample_cnt += frame.samples
|
|
|
+ if arr.ndim > 1 and arr.shape[0] > 1:
|
|
|
+ arr = np.mean(arr, axis=0)
|
|
|
+ else:
|
|
|
+ arr = arr.reshape(-1)
|
|
|
+ audio_chunks.append(arr)
|
|
|
+ if not audio_chunks:
|
|
|
+ raise ValueError("音频文件无有效帧")
|
|
|
+ audio = np.concatenate(audio_chunks).astype(np.float32)
|
|
|
+ max_val = np.max(np.abs(audio))
|
|
|
+ if max_val > 0:
|
|
|
+ audio = audio / max_val
|
|
|
+ orig_rate = stream.sample_rate if stream.sample_rate else 44100
|
|
|
+ if orig_rate != 16000:
|
|
|
+ new_len = int(sample_cnt * 16000 / orig_rate)
|
|
|
+ audio = np.interp(
|
|
|
+ np.linspace(0, sample_cnt - 1, new_len),
|
|
|
+ np.arange(sample_cnt),
|
|
|
+ audio
|
|
|
+ ).astype(np.float32)
|
|
|
+ duration_sec = len(audio) / 16000.0
|
|
|
+ except Exception as e:
|
|
|
+ logger.error("音频解码失败 %s: %s", file_path, e)
|
|
|
+ raise
|
|
|
+
|
|
|
+ segments, info = _model.transcribe(audio, language=language, beam_size=5)
|
|
|
+ text = "".join(seg.text for seg in segments).strip()
|
|
|
+ return {
|
|
|
+ "text": text,
|
|
|
+ "language": info.language,
|
|
|
+ "language_probability": round(float(info.language_probability), 4),
|
|
|
+ "duration": round(duration_sec, 2),
|
|
|
+ }
|