Files
wechatauto-replica/voice2text.py
T

139 lines
4.2 KiB
Python

#!/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 <xxx.silk>")
sys.exit(1)
print(transcribe_silk(sys.argv[1]))