"""Semantic end-of-turn detection — Smart Turn v3 (pipecat-ai, BSD-2).

A fixed silence timeout is a bad proxy for "done talking": it cuts Ahmed off
mid-thought when he pauses to think, and wastes 400ms when he clearly
finished. Smart Turn listens to the ACTUAL speech (grammar, tone, pace — a
Whisper-Tiny encoder + classifier head, 8M params) and returns the
probability that the turn is complete. ~12ms per call on CPU.

main.py uses it like this: once a SHORT pause is detected, ask the model —
"did he finish the sentence?" If yes → respond immediately (snappier than
the old fixed wait). If no → keep listening through the pause, up to a hard
cap. SMART_TURN=0 disables (falls back to the fixed timeout).

Model: huggingface.co/pipecat-ai/smart-turn-v3 (auto-downloaded, ~30MB).
Preprocessing per the reference inference.py: last 8s of 16kHz audio,
front-padded, Whisper log-mel features, normalized.
"""

from __future__ import annotations

import os

import numpy as np

_MODEL_REPO = "pipecat-ai/smart-turn-v3"
_MODEL_FILE = "smart-turn-v3.2-cpu.onnx"   # 12ms/call; GPU stays free for STT
_SAMPLE_RATE = 16_000
_WINDOW_S = 8


def enabled() -> bool:
    return os.environ.get("SMART_TURN", "1") != "0"


class TurnDetector:
    def __init__(self) -> None:
        import onnxruntime as ort
        from huggingface_hub import hf_hub_download
        from transformers import WhisperFeatureExtractor

        path = hf_hub_download(_MODEL_REPO, _MODEL_FILE)
        so = ort.SessionOptions()
        so.inter_op_num_threads = 1
        so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
        self._session = ort.InferenceSession(
            path, sess_options=so, providers=["CPUExecutionProvider"])
        self._fx = WhisperFeatureExtractor(chunk_length=_WINDOW_S)
        # warm once so the first real call isn't the slow one
        self.completion_prob(np.zeros(_SAMPLE_RATE, dtype=np.float32))

    def completion_prob(self, audio: np.ndarray) -> float:
        """P(turn is complete) for 16kHz mono float32 audio (uses last 8s)."""
        n = _WINDOW_S * _SAMPLE_RATE
        if len(audio) > n:
            audio = audio[-n:]
        feats = self._fx(
            audio, sampling_rate=_SAMPLE_RATE, return_tensors="np",
            padding="max_length", max_length=n, truncation=True,
            do_normalize=True,
        ).input_features.squeeze(0).astype(np.float32)[None, ...]
        out = self._session.run(None, {"input_features": feats})
        # output is shape (1,1); numpy 2 only floats 0-d arrays — flatten
        return float(np.asarray(out[0]).reshape(-1)[0])


_detector: TurnDetector | None = None
_failed = False


def get_detector() -> TurnDetector | None:
    """Lazy singleton; None when disabled or unavailable (fixed timeout is
    the fallback, so a download/model failure can never break listening)."""
    global _detector, _failed
    if not enabled() or _failed:
        return None
    if _detector is None:
        try:
            _detector = TurnDetector()
        except Exception as e:  # noqa: BLE001
            _failed = True
            print(f"  smart-turn unavailable ({e}) — fixed-timeout turns")
    return _detector
