transcriber.py 2.6 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. """faster-whisper 本地 STT 引擎封装"""
  2. import os
  3. import logging
  4. import numpy as np
  5. import av
  6. from faster_whisper import WhisperModel
  7. logger = logging.getLogger(__name__)
  8. _model = None
  9. _loaded = False
  10. def init_model(model_size: str = "base", device: str = "cpu", compute_type: str = "int8"):
  11. """初始化 faster-whisper 模型(启动时调用)"""
  12. global _model, _loaded
  13. if _loaded:
  14. return
  15. os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com")
  16. logger.info("加载 faster-whisper 模型 %s (device=%s, compute=%s)...", model_size, device, compute_type)
  17. _model = WhisperModel(model_size, device=device, compute_type=compute_type)
  18. _loaded = True
  19. logger.info("faster-whisper 模型加载完成")
  20. def transcribe_audio(file_path: str, language: str = "zh") -> dict:
  21. """转写音频文件,返回 {text, language, language_probability, duration}"""
  22. if not _model:
  23. raise RuntimeError("faster-whisper 模型未初始化")
  24. duration_sec = 0.0
  25. try:
  26. with av.open(file_path) as container:
  27. stream = container.streams.audio[0]
  28. audio_chunks = []
  29. sample_cnt = 0
  30. for frame in container.decode(audio=0):
  31. arr = frame.to_ndarray()
  32. sample_cnt += frame.samples
  33. if arr.ndim > 1 and arr.shape[0] > 1:
  34. arr = np.mean(arr, axis=0)
  35. else:
  36. arr = arr.reshape(-1)
  37. audio_chunks.append(arr)
  38. if not audio_chunks:
  39. raise ValueError("音频文件无有效帧")
  40. audio = np.concatenate(audio_chunks).astype(np.float32)
  41. max_val = np.max(np.abs(audio))
  42. if max_val > 0:
  43. audio = audio / max_val
  44. orig_rate = stream.sample_rate if stream.sample_rate else 44100
  45. if orig_rate != 16000:
  46. new_len = int(sample_cnt * 16000 / orig_rate)
  47. audio = np.interp(
  48. np.linspace(0, sample_cnt - 1, new_len),
  49. np.arange(sample_cnt),
  50. audio
  51. ).astype(np.float32)
  52. duration_sec = len(audio) / 16000.0
  53. except Exception as e:
  54. logger.error("音频解码失败 %s: %s", file_path, e)
  55. raise
  56. segments, info = _model.transcribe(audio, language=language, beam_size=1, vad_filter=True)
  57. text = "".join(seg.text for seg in segments).strip()
  58. return {
  59. "text": text,
  60. "language": info.language,
  61. "language_probability": round(float(info.language_probability), 4),
  62. "duration": round(duration_sec, 2),
  63. }