lumicollect / core /model_loader.py
nemabruh404's picture
Create model_loader.py
3c22ffe verified
import threading
import time
import tempfile
import wave
import os
import numpy as np
import librosa
from openwakeword.model import Model
from config import (
MODEL_PATH, SAMPLE_RATE, CHUNK_SIZE,
WARMUP_ITERS, STABLE_COUNT, STABLE_MS, MODEL_TIMEOUT_S
)
# --- Singleton model + trạng thái ---
_model: Model | None = None
_model_ready = threading.Event()
_model_error: str | None = None
def get_model() -> Model | None:
return _model
def is_ready() -> bool:
return _model_ready.is_set() and _model is not None
def wait_until_ready(timeout: float = MODEL_TIMEOUT_S) -> bool:
"""Block cho đến khi model sẵn sàng. Trả về False nếu timeout hoặc lỗi."""
return _model_ready.wait(timeout=timeout) and _model is not None
def get_error() -> str | None:
return _model_error
# -------------------------------------------------------
# BƯỚC 1 — Warm-up librosa (lần đầu librosa.load rất chậm
# do lazy import bên trong, phải kích hoạt trước)
# -------------------------------------------------------
def _warmup_librosa():
t = time.perf_counter()
dummy = np.zeros(SAMPLE_RATE, dtype=np.float32)
with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as f:
tmp_path = f.name
with wave.open(tmp_path, "w") as wf:
wf.setnchannels(1)
wf.setsampwidth(2)
wf.setframerate(SAMPLE_RATE)
wf.writeframes((dummy * 32767).astype(np.int16).tobytes())
librosa.load(tmp_path, sr=SAMPLE_RATE)
os.unlink(tmp_path)
print(f" [loader] librosa warm-up: {(time.perf_counter()-t)*1000:.0f}ms")
# -------------------------------------------------------
# BƯỚC 2 — Load ONNX model
# -------------------------------------------------------
def _load_oww_model() -> Model:
t = time.perf_counter()
m = Model(wakeword_model_paths=[MODEL_PATH])
print(f" [loader] ONNX load: {(time.perf_counter()-t)*1000:.0f}ms")
return m
# -------------------------------------------------------
# BƯỚC 3 — JIT warm-up: chạy predict nhiều lần cho đến khi
# ONNX runtime compile xong và latency ổn định
# (tránh lần predict đầu của user bị chậm bất thường)
# -------------------------------------------------------
def _warmup_jit(model: Model):
stable_count = 0
silence = np.zeros(CHUNK_SIZE, dtype=np.int16)
for i in range(WARMUP_ITERS):
t_iter = time.perf_counter()
model.predict(silence)
elapsed_ms = (time.perf_counter() - t_iter) * 1000
if i < 3:
# Vài iter đầu luôn bất thường, bỏ qua
continue
if elapsed_ms < STABLE_MS:
stable_count += 1
if stable_count >= STABLE_COUNT:
print(f" [loader] JIT stable sau {i+1} iters ({elapsed_ms:.1f}ms/iter)")
return
else:
stable_count = 0
print(f" [loader] JIT warm-up xong {WARMUP_ITERS} iters (chưa stable hoàn toàn)")
# -------------------------------------------------------
# MAIN — chạy trong background thread ngay khi app start
# -------------------------------------------------------
def _boot():
global _model, _model_error
t_total = time.perf_counter()
print("[loader] Bắt đầu khởi động model...")
try:
_warmup_librosa()
model = _load_oww_model()
_warmup_jit(model)
_model = model
print(f"[loader] ✅ Sẵn sàng — tổng boot: {time.perf_counter()-t_total:.1f}s")
except Exception as e:
_model_error = str(e)
print(f"[loader] ❌ Lỗi: {e}")
finally:
# Dù thành công hay lỗi đều set event
# để wait_until_ready() không bị block mãi
_model_ready.set()
def start_loading():
"""Gọi hàm này 1 lần duy nhất trong app.py khi khởi động."""
thread = threading.Thread(target=_boot, daemon=True)
thread.start()