#!/usr/bin/env python # -*- coding: utf-8 -*- """微信语音转文字模块(silk 解码 + faster-whisper ASR)。 链路:.silk → silk_v3_decoder.exe 解码为 .pcm → 加 wav 头 → faster-whisper 转文字。 用法: from voice2text import VoiceToText v2t = VoiceToText() text = v2t.transcribe_silk("/path/to/xxx.silk") # 返回文字,失败返回 None text = v2t.transcribe_voice(username, local_id, db) # 直接从微信库提取并转文字 说明: - whisper 模型懒加载(首次调用才加载,约几十秒;之后常驻内存)。 - 中文识别,small 模型在 CPU 上平衡速度与准确率。 """ from __future__ import annotations import os import subprocess import threading import wave BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DECODER_EXE = os.path.join( BASE_DIR, "third_party", "silk-v3-decoder", "silk_v3_decoder.exe" ) WHISPER_MODEL = os.environ.get("V2T_MODEL", "small") SAMPLE_RATE = 24000 # silk_v3_decoder 默认输出采样率 _model = None _model_lock = threading.Lock() _hf_ready = False def _ensure_hf_env(): """国内环境用 hf-mirror 下载模型,并禁用 xet(避免 401)。""" global _hf_ready if _hf_ready: return os.environ.setdefault("HF_ENDPOINT", "https://hf-mirror.com") os.environ.setdefault("HF_HUB_DISABLE_XET", "1") _hf_ready = True def _get_model(): global _model with _model_lock: if _model is None: _ensure_hf_env() from faster_whisper import WhisperModel _model = WhisperModel(WHISPER_MODEL, device="cpu", compute_type="int8") return _model def _silk_to_pcm(silk_path: str, pcm_path: str) -> bool: if not os.path.exists(DECODER_EXE): return False try: r = subprocess.run( [DECODER_EXE, silk_path, pcm_path, "-quiet"], capture_output=True, timeout=60, ) return r.returncode == 0 and os.path.exists(pcm_path) and os.path.getsize(pcm_path) > 0 except Exception: return False def _pcm_to_wav(pcm_path: str, wav_path: str, sample_rate: int = SAMPLE_RATE): with open(pcm_path, "rb") as f: data = f.read() with wave.open(wav_path, "wb") as w: w.setnchannels(1) w.setsampwidth(2) w.setframerate(sample_rate) w.writeframes(data) def transcribe_silk(silk_path: str, sample_rate: int = SAMPLE_RATE): """把 .silk 语音文件转成文字;失败返回 None。""" if not silk_path or not os.path.exists(silk_path): return None pcm_path = silk_path + ".pcm" wav_path = silk_path + ".wav" try: if not _silk_to_pcm(silk_path, pcm_path): return None _pcm_to_wav(pcm_path, wav_path, sample_rate) segments, _info = _get_model().transcribe( wav_path, language="zh", beam_size=5, ) text = "".join(s.text for s in segments).strip() return text or None except Exception: return None finally: for p in (pcm_path, wav_path): try: if os.path.exists(p): os.remove(p) except OSError: pass def transcribe_voice(username: str, local_id: int, db, save_dir: str = None): """从微信库提取语音并转文字:db 为 WeChatDB 实例。""" try: from wechatauto.media import MediaDownloader md = MediaDownloader(db, save_dir=save_dir) silk = md.download_voice(username, local_id, save_dir=save_dir) if not silk: return None return transcribe_silk(silk) except Exception: return None class VoiceToText: """可复用的语音转文字器(模型懒加载、线程安全)。""" def __init__(self, model: str = None): global WHISPER_MODEL if model: WHISPER_MODEL = model def transcribe_silk(self, silk_path: str): return transcribe_silk(silk_path) def transcribe_voice(self, username: str, local_id: int, db, save_dir: str = None): return transcribe_voice(username, local_id, db, save_dir) if __name__ == "__main__": import sys if len(sys.argv) < 2: print("用法: python voice2text.py ") sys.exit(1) print(transcribe_silk(sys.argv[1]))