Download src/turn_detection/runtime/predictor.py from suvradeepp/tiny-hinglish-turn-detector: direct link, hf CLI and curl.
- Browser
- Download file 7.35 kB
-
https://huggingface.co/suvradeepp/tiny-hinglish-turn-detector/resolve/main/src/turn_detection/runtime/predictor.py
- Command line
-
hf download hf://suvradeepp/tiny-hinglish-turn-detector/src/turn_detection/runtime/predictor.py
-
curl -L -o predictor.py https://huggingface.co/suvradeepp/tiny-hinglish-turn-detector/resolve/main/src/turn_detection/runtime/predictor.py
7.35 kB
| """Endpoint predictor interfaces and ONNX implementation.""" | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import time | |
| from dataclasses import asdict, dataclass, field | |
| from pathlib import Path | |
| from typing import Any, Protocol, runtime_checkable | |
| from .controller import ControllerConfig | |
| from .features import FrontendConfig, log_mel_spectrogram, normalize_waveform, resample_waveform | |
| from .types import Prediction | |
| class EndpointPredictor(Protocol): | |
| def predict(self, audio: Any, sample_rate: int) -> Prediction: | |
| """Estimate the probability that the current user turn is complete.""" | |
| class ModelMetadata: | |
| model_name: str | |
| architecture: str | |
| frontend: FrontendConfig = field(default_factory=FrontendConfig) | |
| threshold: float = 0.60 | |
| controller: ControllerConfig = field(default_factory=ControllerConfig) | |
| input_features_name: str = "input_features" | |
| frame_mask_name: str | None = "frame_mask" | |
| endpoint_output_name: str | None = None | |
| output_type: str = "logits" | |
| model_version: str = "unknown" | |
| development_only: bool = False | |
| training_status: str = "unknown" | |
| data_scope: str | None = None | |
| data_revision: str | None = None | |
| parameter_count: int | None = None | |
| def __post_init__(self) -> None: | |
| if not 0.0 <= self.threshold <= 1.0: | |
| raise ValueError("threshold must be in [0, 1]") | |
| if not math.isclose(self.controller.endpoint_threshold, self.threshold, abs_tol=1e-12): | |
| raise ValueError("controller endpoint_threshold must match threshold") | |
| if self.output_type not in {"logits", "probability"}: | |
| raise ValueError("output_type must be logits or probability") | |
| if self.parameter_count is not None and self.parameter_count <= 0: | |
| raise ValueError("parameter_count must be positive when provided") | |
| def from_path(cls, path: str | Path) -> ModelMetadata: | |
| payload = json.loads(Path(path).read_text(encoding="utf-8")) | |
| frontend = FrontendConfig(**payload.pop("frontend", {})) | |
| controller_payload = payload.pop("controller", None) | |
| if controller_payload is None: | |
| threshold = float(payload.get("threshold", 0.60)) | |
| controller = ControllerConfig( | |
| endpoint_threshold=threshold, | |
| long_pause_threshold=max(0.0, threshold - 0.18), | |
| ) | |
| else: | |
| controller = ControllerConfig(**controller_payload) | |
| return cls(frontend=frontend, controller=controller, **payload) | |
| def to_dict(self) -> dict[str, Any]: | |
| return { | |
| "model_name": self.model_name, | |
| "architecture": self.architecture, | |
| "frontend": self.frontend.to_dict(), | |
| "threshold": self.threshold, | |
| "controller": asdict(self.controller), | |
| "input_features_name": self.input_features_name, | |
| "frame_mask_name": self.frame_mask_name, | |
| "endpoint_output_name": self.endpoint_output_name, | |
| "output_type": self.output_type, | |
| "model_version": self.model_version, | |
| "development_only": self.development_only, | |
| "training_status": self.training_status, | |
| "data_scope": self.data_scope, | |
| "data_revision": self.data_revision, | |
| "parameter_count": self.parameter_count, | |
| } | |
| class OnnxEndpointPredictor: | |
| """Batch-one ONNX Runtime predictor with serialized preprocessing contract.""" | |
| def __init__( | |
| self, | |
| model_path: str | Path, | |
| metadata_path: str | Path | None = None, | |
| *, | |
| intra_op_threads: int = 1, | |
| ) -> None: | |
| try: | |
| import onnxruntime as ort | |
| except ImportError as exc: # pragma: no cover - optional dependency | |
| raise RuntimeError("Install the 'demo' or 'export' extra for ONNX inference") from exc | |
| self.model_path = Path(model_path) | |
| if not self.model_path.is_file(): | |
| raise FileNotFoundError(self.model_path) | |
| metadata_file = ( | |
| Path(metadata_path) | |
| if metadata_path | |
| else self.model_path.with_name("model_metadata.json") | |
| ) | |
| self.metadata = ModelMetadata.from_path(metadata_file) | |
| options = ort.SessionOptions() | |
| options.intra_op_num_threads = intra_op_threads | |
| options.inter_op_num_threads = 1 | |
| options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL | |
| self.session = ort.InferenceSession( | |
| str(self.model_path), | |
| sess_options=options, | |
| providers=["CPUExecutionProvider"], | |
| ) | |
| self._input_names = {item.name for item in self.session.get_inputs()} | |
| def predict(self, audio: Any, sample_rate: int) -> Prediction: | |
| import numpy as np | |
| started = time.perf_counter_ns() | |
| features, frame_mask = log_mel_spectrogram(audio, sample_rate, self.metadata.frontend) | |
| feeds = {self.metadata.input_features_name: features[None, :, :]} | |
| if self.metadata.frame_mask_name and self.metadata.frame_mask_name in self._input_names: | |
| feeds[self.metadata.frame_mask_name] = frame_mask[None, :] | |
| output_names = ( | |
| [self.metadata.endpoint_output_name] if self.metadata.endpoint_output_name else None | |
| ) | |
| raw = self.session.run(output_names, feeds)[0] | |
| value = float(np.asarray(raw).reshape(-1)[0]) | |
| probability = ( | |
| 1.0 / (1.0 + math.exp(-value)) if self.metadata.output_type == "logits" else value | |
| ) | |
| elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000 | |
| return Prediction( | |
| endpoint_probability=min(1.0, max(0.0, probability)), | |
| inference_ms=elapsed_ms, | |
| model_name=self.metadata.model_name, | |
| ) | |
| class HeuristicDevelopmentPredictor: | |
| """Clearly labelled fallback used only when exported weights are absent. | |
| It exists so the UI and controller can be exercised before training. Scores | |
| from this class must never be reported as model results. | |
| """ | |
| def predict(self, audio: Any, sample_rate: int) -> Prediction: | |
| import numpy as np | |
| started = time.perf_counter_ns() | |
| samples = resample_waveform(normalize_waveform(audio), sample_rate, 16_000) | |
| tail = samples[-4_000:] if len(samples) >= 4_000 else samples | |
| previous = samples[-12_000:-4_000] if len(samples) >= 12_000 else samples | |
| tail_rms = float(np.sqrt(np.mean(tail * tail) + 1e-12)) | |
| previous_rms = float(np.sqrt(np.mean(previous * previous) + 1e-12)) | |
| drop = max(0.0, min(1.0, 1.0 - tail_rms / max(previous_rms, 1e-4))) | |
| duration_signal = min(1.0, len(samples) / 16_000 / 2.0) | |
| probability = 0.15 + 0.55 * drop + 0.20 * duration_signal | |
| elapsed_ms = (time.perf_counter_ns() - started) / 1_000_000 | |
| return Prediction( | |
| endpoint_probability=min(0.95, max(0.05, probability)), | |
| inference_ms=elapsed_ms, | |
| model_name="heuristic-development-only", | |
| ) | |
| def load_predictor(model_path: str | Path | None) -> EndpointPredictor: | |
| if model_path is not None and Path(model_path).is_file(): | |
| return OnnxEndpointPredictor(model_path) | |
| return HeuristicDevelopmentPredictor() | |