Ctrl K
ComunidadGuías…

butler/gateway/speech_stt.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Inbound voice STT — iLink text preferred; local fallback for .silk (MiniMax has no public ASR)."""

from __future__ import annotations

import logging
import shutil
import subprocess
import tempfile
from pathlib import Path
from typing import cast

logger = logging.getLogger(__name__)


def stt_provider() -> str:
    from butler.configuration.gateway import resolve_gateway_inbound_config

    return cast(str, resolve_gateway_inbound_config().speech.stt_provider)


def silk_to_wav(silk_path: Path, wav_path: Path) -> None:
    ffmpeg = shutil.which("ffmpeg")
    if not ffmpeg:
        raise RuntimeError("未找到 ffmpeg,无法转写微信语音")
    proc = subprocess.run(
        [
            ffmpeg,
            "-y",
            "-i",
            str(silk_path),
            "-ar",
            "16000",
            "-ac",
            "1",
            str(wav_path),
        ],
        capture_output=True,
        text=True,
        timeout=120,
    )
    if proc.returncode != 0:
        err = (proc.stderr or proc.stdout or "").strip()[-400:]
        raise RuntimeError(f"ffmpeg 转码失败: {err}")


def transcribe_wav_local(wav_path: Path) -> str:
    """Optional faster-whisper (``pip install faster-whisper``)."""
    try:
        from faster_whisper import WhisperModel  # type: ignore[import-not-found]
    except ImportError as exc:
        raise RuntimeError(
            "未安装 faster-whisper,无法转写纯语音文件(pip install faster-whisper)"
        ) from exc

    from butler.configuration.gateway import resolve_gateway_inbound_config

    model_size = resolve_gateway_inbound_config().speech.whisper_model
    model = WhisperModel(model_size, device="cpu", compute_type="int8")
    segments, _info = model.transcribe(str(wav_path), language="zh")
    parts = [seg.text.strip() for seg in segments if seg.text.strip()]
    text = "".join(parts).strip()
    if not text:
        raise RuntimeError("语音转写结果为空")
    return text


def transcribe_voice_file(path: str) -> str:
    """Transcribe cached WeChat voice (.silk or .wav)."""
    import time

    t0 = time.monotonic()
    provider = stt_provider()
    if provider in ("off", "none", "0"):
        raise RuntimeError("语音转写已关闭(BUTLER_WECHAT_STT_PROVIDER=off)")

    src = Path(path).expanduser().resolve()
    if not src.is_file():
        raise FileNotFoundError(str(src))

    from butler.gateway.speech_stt_ops import transcribe_with_stt_telemetry

    def _run() -> str:
        with tempfile.TemporaryDirectory(prefix="butler-stt-") as tmp:
            wav = Path(tmp) / "voice.wav"
            if src.suffix.lower() == ".wav":
                wav = src
            else:
                silk_to_wav(src, wav)
            if provider == "local":
                return transcribe_wav_local(wav)
            raise RuntimeError(f"未知 STT provider: {provider}")

    return cast(str, transcribe_with_stt_telemetry(_run, provider=provider, t0=t0))