"""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)