"""PAC ASR 服务 — SenseVoice-small(sherpa-onnx,纯 CPU)。

听写转文字:浏览器录音(webm/opus、mp4 等)→ ffmpeg 解码 16k 单声道 → SenseVoice 离线识别。
模型不打进镜像 —— 挂载卷 SENSEVOICE_DIR(默认 /models/sensevoice,需含 model.int8.onnx + tokens.txt)。
内网专用(compose 网络内供 pac-service 调),无鉴权 —— 不要对外发布端口。
"""
import os
import subprocess
import tempfile
import wave

import numpy as np
import sherpa_onnx
from fastapi import FastAPI, File, HTTPException, UploadFile

MODEL_DIR = os.environ.get("SENSEVOICE_DIR", "/models/sensevoice")
NUM_THREADS = int(os.environ.get("ASR_THREADS", "2"))
MAX_BYTES = 15 * 1024 * 1024  # 听写片段上限 ~15MB(几分钟足够)

recognizer = sherpa_onnx.OfflineRecognizer.from_sense_voice(
    model=os.path.join(MODEL_DIR, "model.int8.onnx"),
    tokens=os.path.join(MODEL_DIR, "tokens.txt"),
    use_itn=True,        # 数字/标点反归一(电话号、日期更可读)
    language="auto",     # zh/en/粤 自动
    num_threads=NUM_THREADS,
)

# 预热:onnxruntime 首次推理要建图/分配,比稳态慢很多 → 启动时拿 0.5s 静音跑一遍,
# 用户的第一句不再付这笔"冷启动"(2026-06-10 服务器实测体感:首句明显慢)。
_warm = recognizer.create_stream()
_warm.accept_waveform(16000, np.zeros(8000, dtype=np.float32))
recognizer.decode_stream(_warm)
del _warm

app = FastAPI()


@app.get("/health")
def health() -> dict:
    return {"ok": True, "model": "sensevoice-small-int8"}


@app.post("/transcribe")
async def transcribe(file: UploadFile = File(...)) -> dict:
    raw = await file.read()
    if not raw:
        raise HTTPException(400, "empty audio")
    if len(raw) > MAX_BYTES:
        raise HTTPException(413, "audio too large")

    suffix = os.path.splitext(file.filename or "")[1] or ".webm"
    src = wav = None
    try:
        with tempfile.NamedTemporaryFile(suffix=suffix, delete=False) as f:
            f.write(raw)
            src = f.name
        wav = src + ".wav"
        # 任意浏览器容器格式 → 16k 单声道 wav(失败=非音频/损坏)
        p = subprocess.run(
            ["ffmpeg", "-y", "-i", src, "-ar", "16000", "-ac", "1", "-f", "wav", wav],
            capture_output=True,
            timeout=60,
        )
        if p.returncode != 0:
            raise HTTPException(400, f"audio decode failed: {p.stderr.decode()[-200:]}")

        with wave.open(wav) as w:
            sr = w.getframerate()
            samples = np.frombuffer(w.readframes(w.getnframes()), dtype=np.int16).astype(np.float32) / 32768.0
        if len(samples) < sr // 5:  # <0.2s 视为误触
            return {"text": "", "durationSec": round(len(samples) / sr, 2)}

        s = recognizer.create_stream()
        s.accept_waveform(sr, samples)
        recognizer.decode_stream(s)
        return {"text": s.result.text, "durationSec": round(len(samples) / sr, 2)}
    finally:
        for fp in (src, wav):
            if fp:
                try:
                    os.unlink(fp)
                except OSError:
                    pass
