poolcoach / app /localcv.py
masterdanh's picture
deploy: snapshot for HF Space
78738de
Raw
History Blame Contribute Delete
5.07 kB
"""CV worker chạy TRONG tiến trình app — mode "local" (14/08/2026, việc B).
Vì sao tồn tại: Space HF free là MỘT container. Bản deploy 23/07 chỉ có app
gợi ý nên không cần gì thêm, còn Analyzer cần detector + ShotNet + segment.
Ba đường đi được cân:
1. Cài redis-server + chạy cv_worker (và cả engine_worker) làm tiến trình
phụ trong container — giữ nguyên code path, nhưng bật ``REDIS_URL`` là
``/api/recommend`` cũng đòi engine worker, thành ra phải nuôi BA tiến
trình + một binary Redis, và hai lần JIT pooltool trên 2 vCPU.
2. Viết một hàng đợi in-memory mới — thêm một bản hiện thực thứ hai của
transport, tức thêm một chỗ để lệch khỏi bản Redis đã nghiệm thu.
3. (chọn) fakeredis trong tiến trình + LUỒNG worker gọi ĐÚNG hàm của
``scripts/cv_worker.py``. Không tiến trình phụ, không binary, không bản
sao logic: ``handle_scan``/``handle_analyze``/``handle_segment`` và vòng
``serve_forever`` là CÙNG code chạy trên máy có GPU.
Đánh đổi đã biết, ghi rõ để đừng ngộ nhận: một luồng phục vụ tuần tự, nên
đúng như bản Redis "trong lúc một analyze chạy, scan job phải chờ" — demo
một người dùng (nếp BG24). GIL nhả trong numpy/torch/cv2 nên API vẫn trả
lời được trong lúc worker chạy, nhưng có tranh CPU: số đo thời gian trên
Space KHÔNG so ngang với số đo worker riêng máy GPU.
Bật bằng env ``POOLCOACH_LOCAL_CV=1`` (xem ``jobqueue.setup``). Không bật
thì file này không được import.
"""
from __future__ import annotations
import importlib.util
import os
import sys
import threading
import time
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
_thread: threading.Thread | None = None
_stop = threading.Event()
_state: dict = {"stage": "off", "error": None, "since": None}
def state() -> dict:
"""Trạng thái luồng worker nhúng — cho log/health đọc, không cho FE."""
return dict(_state)
def _load_worker_module():
"""Nạp ``scripts/cv_worker.py`` như một module.
Import theo ĐƯỜNG DẪN vì repo không cài package và ``scripts/`` không
phải package (quy ước repo — xem CLAUDE.md). Chỉ nạp, không gọi
``main()``: mọi thứ mức module của cv_worker là đẩy sys.path + import
numpy/jobqueue/poolcoach_cv, an toàn khi nạp trong app.
"""
name = "poolcoach_cv_worker"
if name in sys.modules:
return sys.modules[name]
path = ROOT / "scripts" / "cv_worker.py"
spec = importlib.util.spec_from_file_location(name, path)
if spec is None or spec.loader is None:
raise ImportError(f"không nạp được {path}")
mod = importlib.util.module_from_spec(spec)
sys.modules[name] = mod
spec.loader.exec_module(mod)
return mod
def _run() -> None:
t0 = time.time()
try:
_state.update(stage="loading", error=None)
cw = _load_worker_module()
conf = float(os.environ.get("POOLCOACH_CV_CONF", cw.OP_CONF))
device = os.environ.get("POOLCOACH_CV_DEVICE", "")
predict_kw = {"device": device} if device else {}
model, shotnet, resim = cw.load_all(conf, device, predict_kw)
_state.update(stage="serving", since=round(time.time() - t0, 1))
print(f"[poolcoach] CV worker nhung san sang sau "
f"{time.time() - t0:.1f}s -- tab Analyzer dung duoc",
flush=True)
cw.serve_forever(model, conf, predict_kw, shotnet, resim,
should_stop=_stop.is_set)
except BaseException as e: # noqa: BLE001 — luồng chết ≠ app chết
_state.update(stage="error", error=f"{type(e).__name__}: {e}")
print(f"[poolcoach] LOI CV worker nhung: {type(e).__name__}: {e} "
f"-- tab Analyzer se bao 'CV worker khong chay', phan con lai "
f"cua app van chay", flush=True)
finally:
if _state.get("stage") != "error":
_state.update(stage="stopped")
def start() -> None:
"""Bật luồng worker nhúng (daemon). Gọi ở lifespan khi mode == local.
Không chặn startup: nạp YOLO + ShotNet + JIT pooltool mất hàng chục
giây, nhưng trang danh sách cú của video mẫu ĐÃ PHÂN TÍCH SẴN đọc từ
file nên dùng được ngay — heartbeat chỉ bật sau khi nạp xong, đúng nếp
"không báo alive lúc còn đang khởi động".
"""
global _thread
if _thread is not None and _thread.is_alive():
return
_stop.clear()
_thread = threading.Thread(target=_run, daemon=True,
name="poolcoach-localcv")
_thread.start()
def stop(timeout: float = 10.0) -> None:
"""Xin luồng dừng ở vòng lặp kế tiếp (job đang chạy được chạy nốt)."""
_stop.set()
if _thread is not None and _thread.is_alive():
_thread.join(timeout=timeout)