#!/usr/bin/env python3
"""
SenseVoice int8 语音识别 Demo
================================
功能：麦克风录音 → Silero VAD 语音分段 → SenseVoice 识别（带标点）→ 实时显示
同时监控并输出 CPU / 内存占用

用法：
    python3 demo/sense_voice_demo.py

首次运行会自动下载模型（约 228MB），请耐心等待。

按 Ctrl+C 退出时会输出性能统计。
"""

import sys
import os
import time
import argparse
import threading
import queue
import signal
from pathlib import Path

import numpy as np

# ── ANSI 颜色 ─────────────────────────────────────────────────
C = {
    "reset":  "\033[0m",
    "bold":   "\033[1m",
    "dim":    "\033[2m",
    "green":  "\033[32m",
    "cyan":   "\033[36m",
    "yellow": "\033[33m",
    "red":    "\033[31m",
    "magenta":"\033[35m",
    "blue":   "\033[94m",
}

def color(s, c):
    return f"{c}{s}{C['reset']}"

# ── 模型下载 ─────────────────────────────────────────────────
MODEL_DIR = Path(__file__).parent / "models"
SENSEVOICE_URL = (
    "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/"
    "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17.tar.bz2"
)
SILERO_VAD_URL = (
    "https://github.com/k2-fsa/sherpa-onnx/releases/download/asr-models/"
    "silero_vad.onnx"
)

SENSEVOICE_DIR = MODEL_DIR / "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-int8-2024-07-17"
SENSEVOICE_MODEL = SENSEVOICE_DIR / "model.int8.onnx"
SENSEVOICE_TOKENS = SENSEVOICE_DIR / "tokens.txt"
SILERO_VAD_MODEL = MODEL_DIR / "silero_vad.onnx"


def download_with_progress(url, dest):
    """下载文件并显示进度条"""
    import urllib.request
    import shutil

    dest.parent.mkdir(parents=True, exist_ok=True)

    print(f"\n  📥 下载 {Path(dest).name} ...")
    print(f"     {url}")

    try:
        with urllib.request.urlopen(url) as resp:
            total = int(resp.headers.get("Content-Length", 0))
            downloaded = 0
            block_size = 8192
            with open(dest, "wb") as f:
                while True:
                    chunk = resp.read(block_size)
                    if not chunk:
                        break
                    f.write(chunk)
                    downloaded += len(chunk)
                    if total > 0:
                        pct = downloaded / total * 100
                        bar_len = 30
                        filled = int(bar_len * downloaded / total)
                        bar = "█" * filled + "░" * (bar_len - filled)
                        size_mb = downloaded / (1024 * 1024)
                        total_mb = total / (1024 * 1024)
                        print(
                            f"\r     [{bar}] {pct:5.1f}%  {size_mb:.0f}/{total_mb:.0f}MB",
                            end="", flush=True,
                        )
            print()
    except Exception as e:
        if dest.exists():
            dest.unlink()
        raise e


def ensure_models():
    """确保模型文件存在，不存在则下载"""
    import tarfile

    MODEL_DIR.mkdir(parents=True, exist_ok=True)

    # SenseVoice
    if not SENSEVOICE_MODEL.exists():
        archive = MODEL_DIR / "sense_voice_int8.tar.bz2"
        if not archive.exists():
            download_with_progress(SENSEVOICE_URL, archive)

        print(f"  📦 解压模型...")
        with tarfile.open(archive, "r:bz2") as tar:
            tar.extractall(path=MODEL_DIR)
        archive.unlink()
        print(f"  ✅ SenseVoice 模型就绪 ({SENSEVOICE_MODEL.stat().st_size / 1024 / 1024:.0f}MB)")

    # Silero VAD
    if not SILERO_VAD_MODEL.exists():
        download_with_progress(SILERO_VAD_URL, SILERO_VAD_MODEL)
        print(f"  ✅ Silero VAD 模型就绪")

    print()


# ── 性能监控 ─────────────────────────────────────────────────
class PerfMonitor:
    """后台线程采集 CPU / 内存"""

    def __init__(self, interval=1.0):
        self.interval = interval
        self._stop = threading.Event()
        self._thread = None
        self._lock = threading.Lock()

        # 累计统计
        self.cpu_percent_samples = []
        self.rss_mb_samples = []
        self.last_cpu = 0.0
        self.last_rss_mb = 0.0

        try:
            import psutil
            self._psutil = psutil
            self._proc = psutil.Process()
            self._available = True
        except ImportError:
            self._available = False

    @property
    def available(self):
        return self._available

    def start(self):
        if not self._available:
            return
        self._stop.clear()
        self._thread = threading.Thread(target=self._run, daemon=True)
        self._thread.start()

    def stop(self):
        self._stop.set()
        if self._thread:
            self._thread.join(timeout=2)

    def _run(self):
        while not self._stop.is_set():
            try:
                cpu = self._proc.cpu_percent(interval=None)
                rss_mb = self._proc.memory_info().rss / 1024 / 1024
            except Exception:
                cpu, rss_mb = 0, 0
            with self._lock:
                self.last_cpu = cpu
                self.last_rss_mb = rss_mb
                self.cpu_percent_samples.append(cpu)
                self.rss_mb_samples.append(rss_mb)
            self._stop.wait(self.interval)

    def current(self):
        with self._lock:
            return self.last_cpu, self.last_rss_mb

    def summary(self):
        with self._lock:
            if not self.cpu_percent_samples:
                return "无数据"
            cpu_avg = np.mean(self.cpu_percent_samples)
            cpu_max = np.max(self.cpu_percent_samples)
            rss_avg = np.mean(self.rss_mb_samples)
            rss_max = np.max(self.rss_mb_samples)
            return {
                "cpu_avg": cpu_avg,
                "cpu_max": cpu_max,
                "rss_avg_mb": rss_avg,
                "rss_max_mb": rss_max,
                "samples": len(self.cpu_percent_samples),
            }


# ── 识别统计 ─────────────────────────────────────────────────
class ASRStats:
    """记录每次识别的性能"""

    def __init__(self):
        self._lock = threading.Lock()
        self.segments = []  # list of (audio_duration, asr_time, text)

    def record(self, audio_duration, asr_time, text):
        with self._lock:
            self.segments.append((audio_duration, asr_time, text))

    def summary(self):
        with self._lock:
            if not self.segments:
                return None
            total_audio = sum(s[0] for s in self.segments)
            total_asr = sum(s[1] for s in self.segments)
            total_text = sum(len(s[2]) for s in self.segments)
            rtf = total_asr / total_audio if total_audio > 0 else 0
            return {
                "segments": len(self.segments),
                "total_audio_s": total_audio,
                "total_asr_s": total_asr,
                "total_chars": total_text,
                "rtf": rtf,
                "speedup": 1 / rtf if rtf > 0 else float("inf"),
            }


# ── 主程序 ────────────────────────────────────────────────────
def main():
    parser = argparse.ArgumentParser(description="SenseVoice int8 语音识别 Demo")
    parser.add_argument("--device", type=int, default=None,
                        help="音频输入设备编号（不指定则用默认）")
    parser.add_argument("--list-devices", action="store_true",
                        help="列出所有音频设备后退出")
    parser.add_argument("--save-audio", type=str, default=None,
                        help="保存最后一次识别的音频到指定文件（用于调试）")
    parser.add_argument("--num-threads", type=int, default=2,
                        help="ONNX 推理线程数（默认 2）")
    parser.add_argument("--vad-threshold", type=float, default=0.5,
                        help="VAD 灵敏度 0~1（默认 0.5，越小越敏感）")
    parser.add_argument("--silence-duration", type=float, default=0.8,
                        help="判定为句尾的静音时长秒数（默认 0.8）")
    parser.add_argument("--max-speech", type=float, default=30,
                        help="单段最长语音秒数（默认 30）")
    args = parser.parse_args()

    # ── 列出设备 ──
    import sounddevice as sd
    if args.list_devices:
        print("\n🎤 可用音频设备：\n")
        devices = sd.query_devices()
        for i, d in enumerate(devices):
            in_ch = d["max_input_channels"]
            if in_ch > 0:
                print(f"  [{i}] {d['name']} (输入通道: {in_ch}, 默认采样率: {d['default_samplerate']:.0f})")
        print()
        return

    # ── 下载模型 ──
    print(color("\n🔧 检查模型文件...", C["cyan"]))
    ensure_models()

    # ── 初始化 sherpa-onnx ──
    print(color("🚀 初始化识别引擎...", C["cyan"]))
    import sherpa_onnx

    # 通过工厂方法创建 SenseVoice 识别器
    recognizer = sherpa_onnx.OfflineRecognizer.from_sense_voice(
        model=str(SENSEVOICE_MODEL),
        tokens=str(SENSEVOICE_TOKENS),
        num_threads=args.num_threads,
        provider="cpu",
        language="auto",
        use_itn=True,  # 启用标点 + 逆文本正则化
    )

    # Silero VAD 配置
    silero_vad_config = sherpa_onnx.SileroVadModelConfig(
        model=str(SILERO_VAD_MODEL),
        threshold=args.vad_threshold,
        min_silence_duration=args.silence_duration,
        min_speech_duration=0.25,
        max_speech_duration=args.max_speech,
    )

    vad_config = sherpa_onnx.VadModelConfig(
        silero_vad=silero_vad_config,
        sample_rate=16000,
        num_threads=1,
    )

    vad = sherpa_onnx.VoiceActivityDetector(vad_config, buffer_size_in_seconds=60)

    print(color("✅ 引擎就绪！", C["green"]))

    # ── 启动性能监控 ──
    perf = PerfMonitor(interval=1.0)
    perf.start()

    asr_stats = ASRStats()

    # ── 音频参数 ──
    SAMPLE_RATE = 16000
    BLOCK_SIZE = 1024  # 每次读取的采样数

    # ── 优雅退出 ──
    running = True

    def on_sigint(sig, frame):
        nonlocal running
        running = False
        print(f"\n{color('⏹  正在退出...', C['yellow'])}")

    signal.signal(signal.SIGINT, on_sigint)

    # ── 打印标题 ──
    print()
    print(color("╔════════════════════════════════════════════════╗", C["bold"]))
    print(color("║     🎙️  SenseVoice 语音识别 Demo              ║", C["bold"]))
    print(color("║     中英混说 · 自带标点 · 低资源运行            ║", C["bold"]))
    print(color("╚════════════════════════════════════════════════╝", C["bold"]))
    print()
    print(f"  {color('🎤', C['cyan'])} 对着麦克风说话，说完停顿即可看到结果")
    print(f"  {color('⏸', C['cyan'])} 按 {color('Ctrl+C', C['yellow'])} 退出并查看统计")
    print(f"  {color('📊', C['cyan'])} VAD 阈值: {args.vad_threshold}  |  静音判定: {args.silence_duration}s")
    print()

    # 状态指示
    STATUS_IDLE = f" {color('⏳', C['dim'])} 等待语音..."
    STATUS_LISTENING = f" {color('🔴', C['red'])} 正在听..."
    STATUS_PROCESSING = f" {color('⚙️', C['yellow'])} 识别中..."

    sys.stdout.write(STATUS_IDLE)
    sys.stdout.flush()

    def audio_callback(indata, frames, time_info, status):
        """麦克风回调：将音频送入 VAD"""
        if status:
            print(f"\n  ⚠️ 音频状态: {status}")
        # 转为 float32 并归一化到 [-1, 1]
        if indata.dtype == np.int16:
            samples = indata[:, 0].astype(np.float32) / 32768.0
        else:
            samples = indata[:, 0].astype(np.float32)
        vad.accept_waveform(samples)

    try:
        with sd.InputStream(
            samplerate=SAMPLE_RATE,
            device=args.device,
            channels=1,
            dtype="float32",
            blocksize=BLOCK_SIZE,
            callback=audio_callback,
        ):
            last_status = STATUS_IDLE
            segment_count = 0

            while running:
                time.sleep(0.05)  # 50ms 轮询

                # 检测是否有语音段结束
                while not vad.empty():
                    speech_segment = vad.front  # sherpa_onnx.SpeechSegment
                    samples = np.array(speech_segment.samples, dtype=np.float32)
                    audio_duration = len(samples) / SAMPLE_RATE

                    vad.pop()

                    # ── 识别 ──
                    sys.stdout.write(f"\r{STATUS_PROCESSING} ({audio_duration:.1f}s 音频)")
                    sys.stdout.flush()

                    t0 = time.perf_counter()
                    stream = recognizer.create_stream()
                    stream.accept_waveform(SAMPLE_RATE, samples)
                    recognizer.decode_stream(stream)
                    text = stream.result.text.strip()
                    t1 = time.perf_counter()

                    asr_time = t1 - t0
                    asr_stats.record(audio_duration, asr_time, text)

                    # ── 显示结果 ──
                    segment_count += 1
                    rtf = asr_time / audio_duration if audio_duration > 0 else 0
                    speed = 1 / rtf if rtf > 0 else float("inf")

                    # 清除状态行
                    sys.stdout.write("\r" + " " * 60 + "\r")

                    # 输出识别文本
                    prefix = color(f"[{segment_count}]", C["dim"])
                    speed_info = color(
                        f"({asr_time:.2f}s / {audio_duration:.1f}s, RTF={rtf:.3f}, {speed:.0f}x)",
                        C["dim"],
                    )
                    print(f"  {prefix} {color(text, C['green'])}")
                    print(f"       {speed_info}")

                    # 保存音频（调试用）
                    if args.save_audio and segment_count == 1:
                        import sherpa_onnx
                        sherpa_onnx.write_wave(
                            str(args.save_audio),
                            samples.tolist(),
                            SAMPLE_RATE,
                        )
                        print(f"       💾 音频已保存到 {args.save_audio}")

                    # 恢复状态
                    sys.stdout.write(STATUS_IDLE)
                    sys.stdout.flush()

                # 更新状态行
                current_status = STATUS_LISTENING if vad.is_speech_detected() else STATUS_IDLE
                if current_status != last_status:
                    sys.stdout.write(f"\r{current_status}")
                    sys.stdout.flush()
                    last_status = current_status

    except KeyboardInterrupt:
        pass
    finally:
        running = False
        perf.stop()

    # ── 输出统计 ──
    print("\n")
    print(color("╔════════════════════════════════════════════════╗", C["bold"]))
    print(color("║               📊 性能统计                      ║", C["bold"]))
    print(color("╚════════════════════════════════════════════════╝", C["bold"]))
    print()

    # ASR 统计
    s = asr_stats.summary()
    if s:
        print(color("  🎙️  识别统计", C["cyan"]))
        print(f"     识别段数:     {s['segments']}")
        print(f"     总音频时长:   {s['total_audio_s']:.1f}s")
        print(f"     总识别耗时:   {s['total_asr_s']:.2f}s")
        print(f"     总字符数:     {s['total_chars']}")
        print(f"     RTF:         {s['rtf']:.4f}")
        print(f"     处理速度:     {s['speedup']:.1f}x 实时")
        print()

    # 系统资源统计
    ps = perf.summary()
    if isinstance(ps, dict):
        print(color("  💻 系统资源", C["cyan"]))
        print(f"     CPU 平均:    {ps['cpu_avg']:.1f}%")
        print(f"     CPU 峰值:    {ps['cpu_max']:.1f}%")
        print(f"     内存平均:    {ps['rss_avg_mb']:.0f}MB")
        print(f"     内存峰值:    {ps['rss_max_mb']:.0f}MB")
        print(f"     采样点数:    {ps['samples']}")
        print()
    elif not perf.available:
        print(color("  ⚠️  未安装 psutil，无法采集 CPU/内存数据", C["yellow"]))
        print(color("     安装: pip3 install psutil", C["dim"]))
        print()


if __name__ == "__main__":
    main()
