diff --git a/docs/issues/ISSUE_164_OCR_VS_YOLO_DIVERGENCE.md b/docs/issues/ISSUE_164_OCR_VS_YOLO_DIVERGENCE.md new file mode 100644 index 0000000..a463ef9 --- /dev/null +++ b/docs/issues/ISSUE_164_OCR_VS_YOLO_DIVERGENCE.md @@ -0,0 +1,395 @@ +# Issue #164: Why OCR Works and YOLO Doesn't — E2E Divergence Report + +**Date:** 2026-02-09 +**Kaggle deploy:** forgesyte-plugins `d8d902b`, forgesyte `b1c91a0` +**Local HEAD:** forgesyte-plugins `ac5b980`, forgesyte `dc02fcb` + +--- + +## 1. The Error (from Kaggle server logs) + +```json +{"timestamp": "2026-02-08T21:41:58.801728+00:00", "name": "forgesyte_yolo_tracker.plugin", "message": "Base64 decode failed in player_detection: 'NoneType' object has no attribute 'startswith'"} +{"timestamp": "2026-02-08T21:41:58.802123+00:00", "name": "app.tasks", "message": "Plugin output normalisation failed", "job_id": "8d9b43ea-c121-4f0a-864b-7e35c5ec09a6", "plugin": "yolo-tracker", "error": "Missing required field: 'boxes'"} +{"timestamp": "2026-02-08T21:41:58.802288+00:00", "name": "app.tasks", "message": "Job updated", "job_id": "8d9b43ea-c121-4f0a-864b-7e35c5ec09a6", "fields": ["status", "result", "completed_at", "progress", "device_used"]} +{"timestamp": "2026-02-08T21:41:58.802383+00:00", "name": "app.tasks", "message": "Job completed successfully", "processing_time_ms": 0.700775999803227, "device_requested": "cpu", "device_used": "cpu"} +``` + +--- + +## 2. Shared Path (Identical for OCR and YOLO) + +Both plugins share the exact same server-side code path from upload to plugin dispatch. + +### Step 1: POST /v1/analyze?plugin= + +**File:** `forgesyte/server/app/api.py` line 117-194 + +```python +@router.post("/analyze", response_model=AnalyzeResponse) +async def analyze_image( + request: Request, + file: Optional[UploadFile] = None, + plugin: str = Query(..., description="Vision plugin identifier"), + image_url: Optional[str] = Query(None, description="URL of image to analyze"), + options: Optional[str] = Query(None, description="JSON string of plugin options"), + device: str = Query("cpu", description="Device to use: 'cpu' or 'gpu'"), + auth: Dict[str, Any] = Depends(require_auth(["analyze"])), + service: AnalysisService = Depends(get_analysis_service), +) -> AnalyzeResponse: + """Submit an image for analysis using specified vision plugin. + + Supports multiple image sources: file upload, remote URL, or raw body bytes. + Returns job ID for asynchronous result tracking via GET /jobs/{job_id}. + + Args: + request: FastAPI request context with body and app state. + file: Optional file upload containing image data. + image_url: Optional HTTP(S) URL to fetch image from. + options: Optional JSON string with plugin-specific configuration. + device: Device to use ("cpu" or "gpu", default "cpu"). + auth: Authentication credentials (required, "analyze" permission). + service: Injected AnalysisService for orchestration. + + Returns: + AnalyzeResponse containing job_id, device info, and frame tracking. + + Raises: + HTTPException: 400 Bad Request if options JSON is invalid. + HTTPException: 400 Bad Request if image URL fetch fails. + HTTPException: 400 Bad Request if image data is invalid. + HTTPException: 400 Bad Request if device parameter is invalid. + HTTPException: 500 Internal Server Error if unexpected failure occurs. + """ + # ... validation ... + result = await service.process_analysis_request( + file_bytes=file_bytes, + image_url=image_url, + body_bytes=await request.body() if not file else None, + plugin=plugin, + options=parsed_options, + device=device.lower(), # ← always "cpu" unless client sends device param + ) +``` + +### Step 2: AnalysisService resolves device, acquires image + +**File:** `forgesyte/server/app/services/analysis_service.py` line 67-144 + +```python +async def process_analysis_request( + self, + file_bytes: Optional[bytes], + image_url: Optional[str], + body_bytes: Optional[bytes], + plugin: str, + options: Dict[str, Any], + device: Optional[str] = None, +) -> Dict[str, Any]: + """Process an image analysis request from multiple possible sources. + + Orchestrates the complete flow: + 1. Determine image source (file, URL, or base64 body) + 2. Acquire image bytes using appropriate method + 3. Validate options JSON + 4. Submit job to task processor with device preference + 5. Return job tracking information + + Args: + file_bytes: Raw bytes from uploaded file (optional) + image_url: URL to fetch image from (optional) + body_bytes: Raw request body containing base64 image (optional) + plugin: Name of plugin to execute + options: Dict of plugin-specific options (already parsed) + device: Device preference ("cpu" or "gpu", default "cpu") + + Returns: + Dictionary with: + - job_id: Unique job identifier + - status: Job status (queued, processing, completed, error) + - plugin: Plugin name used + - image_size: Size of image in bytes + - device_requested: Requested device ("cpu" or "gpu") + + Raises: + ValueError: If no valid image source provided + ValueError: If image data is invalid + ExternalServiceError: If remote image fetch fails after retries + """ + # 1. Acquire image from appropriate source (pass options for JSON base64) + image_bytes = await self._acquire_image(file_bytes, image_url, body_bytes, options) + + if not image_bytes: + logger.error("No image data acquired from any source") + raise ValueError("No valid image provided") + + # 2. Resolve device: request param > options > default cpu + resolved_device = device or options.get("device") or "cpu" + + # 3. Submit job + job_id = await self.processor.submit_job( + image_bytes=image_bytes, # ← raw bytes from upload + plugin_name=plugin, + options=options, + device=resolved_device, # ← "cpu" + ) +``` + +### Step 3: TaskProcessor submits and processes job + +**File:** `forgesyte/server/app/tasks.py` line 261-394 + +```python +async def submit_job( + self, + image_bytes: bytes, + plugin_name: str, + options: Optional[dict[str, Any]] = None, + device: str = "cpu", + callback: Optional[Callable[[dict[str, Any]], Any]] = None, +) -> str: + """Submit a new image analysis job. + + Creates a job record and dispatches it for asynchronous processing + in the background. Returns immediately with the job_id. + + Args: + image_bytes: Raw image data (PNG, JPEG, etc.) + plugin_name: Name of the analysis plugin to use + options: Plugin-specific analysis options (optional) + device: Device preference ("cpu" or "gpu", default "cpu") + callback: Callable invoked when job completes (optional) + + Returns: + Job ID for status tracking and result retrieval + + Raises: + ValueError: If image_bytes is empty or plugin_name is missing + """ + # ... creates job record, dispatches _process_job() ... +``` + +```python +async def _process_job( + self, + job_id: str, + image_bytes: bytes, + plugin_name: str, + options: dict[str, Any], + device: str = "cpu", +) -> None: + """Process a job asynchronously. + + Runs the actual analysis in a thread pool, updates job status, + handles errors, and invokes completion callbacks. + + Args: + job_id: Unique job identifier + image_bytes: Raw image data to analyze + plugin_name: Name of the plugin to run + options: Plugin-specific options + device: Device preference ("cpu" or "gpu") + + Returns: + None + + Raises: + None (catches all exceptions and logs them) + """ + # ... + tool_name = options.get("tool", "default") + tool_args = { + "image_bytes": image_bytes, # ← raw bytes passed through + "options": {k: v for k, v in options.items() if k != "tool"}, + } + # NOTE: device is available in this scope but is NOT added to tool_args + result = await loop.run_in_executor( + self._executor, plugin.run_tool, tool_name, tool_args # ← dispatched to plugin + ) +``` + +**Key fact:** `tool_args` contains `image_bytes` (raw bytes) and `options`. +`device` is NOT included in `tool_args`. + +--- + +## 3. The Divergence Point: `plugin.run_tool()` + +This is where OCR and YOLO take completely different paths. + +### OCR run_tool() — at deployed commit d8d902b + +**File:** `plugins/ocr/src/forgesyte_ocr/plugin.py` line 72-96 + +```python +def run_tool(self, tool_name: str, args: dict[str, Any]) -> Any: + """Execute a tool by name with the given arguments. + + Args: + tool_name: Name of tool to execute. Accepts "default" as alias + for "analyze" for backward compatibility. (WHy do need bckward???????) + args: Tool arguments dict + + Returns: + Tool result (OCROutput) + + Raises: + ValueError: If tool name not found + """ + # Accept "default" as alias for "analyze" (for backward compatibility) + if tool_name in ("default", "analyze"): + image_bytes = args.get("image_bytes") # ← reads "image_bytes" key ✓ + if not isinstance(image_bytes, bytes): # ← validates type ✓ + raise ValueError("image_bytes must be bytes") + return self.analyze( + image_bytes=image_bytes, # ← passes raw bytes to engine ✓ + options=args.get("options"), + ) + raise ValueError(f"Unknown tool: {tool_name}") +``` + +**Result:** OCR reads `args["image_bytes"]` → gets raw bytes → works. + +### YOLO run_tool() — at deployed commit d8d902b + +**File:** `plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py` line 334-370 + +```python +def run_tool(self, tool_name: str, args: Dict[str, Any]) -> Any: + """Execute a tool by name with the given arguments. + + Args: + tool_name: Name of tool to execute. Accepts "default" as alias + for first available tool for backward compatibility (Issue #164). + args: Tool arguments dict + + Returns: + Tool result (dict with detections/keypoints/etc) + + Raises: + ValueError: If tool name not found + """ + # Accept "default" as alias for first tool (backward compatibility - Issue #164) + if tool_name == "default": + tool_name = next(iter(self.tools.keys())) # → "player_detection" + + if tool_name not in self.tools: + raise ValueError(f"Unknown tool: {tool_name}") + + handler = self.tools[tool_name]["handler"] + + # Video tools use different args + if "video" in tool_name: + return handler( + video_path=args.get("video_path"), + output_path=args.get("output_path"), + device=args.get("device", "cpu"), + ) + + # Frame tools use frame_base64 + return handler( + frame_base64=args.get("frame_base64"), # ← reads "frame_base64" key ✗ + device=args.get("device", "cpu"), # server sent "image_bytes" not "frame_base64" + annotated=args.get("annotated", False), # so args.get("frame_base64") returns None + ) +``` + +**Result:** YOLO reads `args["frame_base64"]` → key doesn't exist → gets `None` → crashes. + +--- + +## 4. The Crash Chain + +``` +Server sends: tool_args = {"image_bytes": , "options": {...}} +YOLO reads: args.get("frame_base64") → None +YOLO calls: _tool_player_detection(frame_base64=None, ...) +Which calls: _decode_frame_base64_safe(None, "player_detection") +Which calls: _validate_base64(None) +Which calls: None.startswith("data:image") → 💥 AttributeError +Caught by: except block → logger.warning("Base64 decode failed in player_detection: 'NoneType'...") +Returns: {"error": "invalid_base64", "message": "Failed to decode frame: ..."} +``` + +Then the normaliser fails because the error dict doesn't have a `"boxes"` field. + +--- + +## 5. Side-by-Side Comparison Table + +| Aspect | OCR (works) | YOLO (crashes) | +|--------|-------------|-----------------| +| **Deployed commit** | d8d902b | d8d902b | +| **run_tool reads** | `args.get("image_bytes")` | `args.get("frame_base64")` | +| **Server sends** | `{"image_bytes": }` | `{"image_bytes": }` | +| **Key match?** | YES — both say `image_bytes` | NO — server says `image_bytes`, plugin expects `frame_base64` | +| **Tool name handling** | `"default"` → `"analyze"` (alias) | `"default"` → first tool key (alias) | +| **Input type expected** | `bytes` (raw) | `str` (base64 string) | +| **Manifest input field** | `image_base64` | `frame_base64` | +| **Manifest mode** | `"image"` | not set | +| **Manifest tools format** | list | dict | + +--- + +## 6. Root Cause + +**The server was updated to send `image_bytes` (raw bytes) in tool_args.** +**OCR was updated to read `image_bytes` from args.** +**YOLO was NOT updated — it still reads `frame_base64` from args.** + +The fix on the local machine (commits `7bbf6e2` through `9b52512`) updated YOLO to read +`image_bytes`, but those commits were never pushed to origin. Meanwhile, `d8d902b` was +pushed from a different branch that didn't include those changes. (what branch where are you getting this information?????????) + +--- + +## 7. What models.yaml device: "cuda" Has To Do With It + +Separate issue. Even if the key mismatch is fixed, the `device` from `models.yaml` +is never used in the request pipeline: + +- Server defaults to `"cpu"` (api.py line 124) +- Server does NOT include `device` in `tool_args` (tasks.py line 388-391) +- Plugin falls back to `"cpu"` when `device` not in args (plugin.py run_tool) +- `models.yaml` `device: "cuda"` is read by `load_model_config()` but never called + in the request path + +--- + +## 8. What `"mode"` Field Does + +- OCR manifest has `"mode": "image"` — but the server does NOT read this field +- `PluginMetadata` model (server/app/models.py line 76) has no `mode` field +- Plugin loader does not check `mode` +- Adding `"mode"` to YOLO manifest is good practice for documentation and + future routing but does NOT fix the current crash + +--- + +## 9. Fix Required (Phase 12 / #164) + +The deployed YOLO plugin must be updated so `run_tool()` reads `args.get("image_bytes")` +instead of `args.get("frame_base64")`. This is the ONLY change needed to make YOLO +work through the same path as OCR. + +The local codebase (ac5b980) already has this fix in plugin.py. It needs to be +deployed to Kaggle. + +Additionally, the manifest.json should be updated to reflect the actual input +contract (`image_bytes` not `frame_base64`), and the validator should enforce +consistency. + +--- + +## 10. Files Involved + +| File | Repo | Role | +|------|------|------| +| `server/app/api.py` L117-194 | forgesyte | POST /v1/analyze endpoint | +| `server/app/services/analysis_service.py` L107-144 | forgesyte | Image acquisition + job submission | +| `server/app/tasks.py` L380-394 | forgesyte | Builds tool_args, dispatches run_tool | +| `plugins/ocr/src/forgesyte_ocr/plugin.py` L72-96 | forgesyte-plugins | OCR run_tool (reads image_bytes ✓) | +| `plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py` L334-370 | forgesyte-plugins | YOLO run_tool (reads frame_base64 ✗ at d8d902b) | +| `plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/manifest.json` | forgesyte-plugins | Declares frame_base64 (should be image_bytes) | +| `validate_manifest.py` | forgesyte-plugins | Validates manifest structure | +| `plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/configs/models.yaml` | forgesyte-plugins | device: "cuda" (never read in request path) | diff --git a/docs/issues/ISSUE_164_PHASE_12_ROOT_CAUSE_ANALYSIS.md b/docs/issues/ISSUE_164_PHASE_12_ROOT_CAUSE_ANALYSIS.md new file mode 100644 index 0000000..cab45b4 --- /dev/null +++ b/docs/issues/ISSUE_164_PHASE_12_ROOT_CAUSE_ANALYSIS.md @@ -0,0 +1,162 @@ +# Issue #164: Phase 12 Root Cause Analysis — Multiple Divergences + +**Date:** 2026-02-09 +**Main branches:** forgesyte-plugins `ac5b980`, forgesyte `b1c91a0` +**Kaggle error:** `ValueError: Unknown tool: default` + +--- + +## Executive Summary + +Three distinct issues prevent YOLO plugin from working: + +1. **Tool name mismatch** (NEW) - Server sends `"default"`, plugin rejects it +2. **Image bytes key mismatch** - Server sends `image_bytes`, manifest declares `frame_base64` +3. **Device hardcoding** - Device config in models.yaml never used + +--- + +## Issue 1: Tool Name Mismatch (BLOCKING) + +### Current State (2026-02-09) + +**Server (forgesyte/main/b1c91a0)** — [tasks.py L387](file:///home/rogermt/forgesyte/server/app/tasks.py#L387) +```python +tool_name = options.get("tool", "default") # Always sends "default" +tool_args = { + "image_bytes": image_bytes, + "options": {k: v for k, v in options.items() if k != "tool"}, +} +result = await loop.run_in_executor( + self._executor, plugin.run_tool, tool_name, tool_args +) +``` +→ Passes `tool_name="default"` + +**Plugin (forgesyte-plugins/main/ac5b980)** — [plugin.py L341-342](file:///home/rogermt/forgesyte-plugins/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py#L341-L342) +```python +def run_tool(self, tool_name: str, args: dict[str, Any]) -> Any: + if tool_name not in self.tools: + raise ValueError(f"Unknown tool: {tool_name}") # ← REJECTS "default" +``` +→ `self.tools = {"player_detection": {...}, "player_tracking": {...}, ...}` +→ `"default"` is NOT in tools dict → **ValueError** + +### Root Cause + +The plugin alias handling was **removed** (cleaned up) but the server still sends `"default"`. + +**Old code (before ac5b980):** +```python +if tool_name == "default": + tool_name = next(iter(self.tools.keys())) # Maps "default" → "player_detection" +``` + +**Current code (ac5b980):** +- Alias logic deleted +- Direct tool name lookup only +- Server not updated + +### Kaggle Error Log + +```json +{ + "timestamp": "2026-02-09T18:30:47.176914+00:00", + "name": "app.tasks", + "message": "Job failed with exception", + "exc_info": "...plugin.py\", line 342, in run_tool\n raise ValueError(f\"Unknown tool: {tool_name}\")\nValueError: Unknown tool: default" +} +``` + +--- + +## Issue 2: Image Bytes Key Mismatch (SECONDARY) + +### Current State + +**Server sends:** `{"image_bytes": , ...}` ✓ +**Plugin expects:** Reads `args.get("image_bytes")` ✓ +**Manifest declares:** `"frame_base64": "string"` ✗ + +**The manifest is outdated** — it declares `frame_base64` input but plugin reads `image_bytes`. + +This won't cause a crash (plugin validates type correctly at L356), but the manifest is incorrect documentation. + +--- + +## Issue 3: Device Hardcoding (TERTIARY) + +**Problem:** Device defaults to `"cpu"` throughout, ignoring models.yaml config. + +**Code path:** +1. Server defaults to "cpu" (api.py L124) +2. Server does NOT pass device in tool_args (tasks.py L388-391) +3. Plugin falls back to "cpu" (plugin.py L364) +4. models.yaml device: "cuda" is never read in request path + +--- + +## Fix Priority + +### Immediate (P0 - Blocking) + +**Fix Issue 1:** Tool name mismatch + +**Option A: Server sends proper tool name** +- Change tasks.py L387: `tool_name = options.get("tool", "player_detection")` +- Pros: Server decides default tool name +- Cons: Hard to change later + +**Option B: Plugin accepts "default" alias** +- Add back: `if tool_name == "default": tool_name = next(iter(self.tools.keys()))` +- Pros: Backward compatible +- Cons: Keeps "legacy" code (see #99) + +**Recommended:** Option A (Phase 12 removes aliases per #99) + +### Secondary (P1 - Documentation) + +**Fix Issue 2:** Update manifest to match code + +**Change:** All tool inputs from `"frame_base64": "string"` to `"image_bytes": "bytes"` + +### Tertiary (P2 - Enhancement) + +**Fix Issue 3:** Pass device through pipeline + +**See:** GitHub issue #100 (device from models.yaml) + +--- + +## Files Requiring Changes + +| File | Repo | Issue | Action | +|------|------|-------|--------| +| `server/app/tasks.py` L387 | forgesyte | #1 | Change default tool_name | +| `plugins/.../plugin.py` | forgesyte-plugins | #1 | DONE (already expects non-default) | +| `plugins/.../manifest.json` | forgesyte-plugins | #2 | Update input schema | +| `server/app/api.py` | forgesyte | #3 | Pass device in tool_args | +| `plugins/.../plugin.py` | forgesyte-plugins | #3 | Use device from args | + +--- + +## Testing Strategy + +1. **Local (CPU):** Run contract tests with corrected tool_name +2. **Kaggle (GPU):** Deploy and test with real YOLO models +3. **Web-UI:** Verify tool selector sends proper tool name (not "default") + +--- + +## Timeline + +- **Phase 12:** Fix #1 + #2 (tool name + manifest) — BLOCKING +- **Phase 13:** Fix #3 (device handling) — ENHANCEMENT + +--- + +## Related Issues + +- #99 - Remove backward compatibility aliases (already implemented) +- #100 - Use device from models.yaml +- #164 - Original YOLO crash report (multiple root causes) diff --git a/plugins/forgesyte-yolo-tracker/pyproject.toml b/plugins/forgesyte-yolo-tracker/pyproject.toml index 84d3a21..204ab65 100644 --- a/plugins/forgesyte-yolo-tracker/pyproject.toml +++ b/plugins/forgesyte-yolo-tracker/pyproject.toml @@ -46,11 +46,11 @@ packages = ["forgesyte_yolo_tracker"] package-dir = {"" = "src"} [tool.black] -line-length = 100 +line-length = 88 target-version = ["py39"] [tool.ruff] -line-length = 100 +line-length = 88 target-version = "py39" [tool.coverage.run] diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/configs/__init__.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/configs/__init__.py index 1f14956..08fd410 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/configs/__init__.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/configs/__init__.py @@ -1,4 +1,10 @@ -"""Configs module for YOLO Tracker.""" +"""Configs module for YOLO Tracker. + +Phase 12: Strict device governance. +- YAML is the single source of truth for models, confidence, device. +- No in-code fallbacks. +- Device is required in YAML; if missing, raise ConfigError. +""" from pathlib import Path from typing import Any, Dict @@ -6,72 +12,86 @@ import yaml __all__ = [ - "SoccerPitchConfiguration", + "ConfigError", "load_model_config", "get_model_path", "get_confidence", + "get_device", "get_default_detections", "MODEL_CONFIG_PATH", + "_reset_config_cache_for_tests", ] # Path to the models configuration file MODEL_CONFIG_PATH = Path(__file__).parent / "models.yaml" -# Default configuration (fallback if YAML file is missing) -DEFAULT_MODEL_CONFIG: Dict[str, Any] = { - "models": { - "player_detection": "football-player-detection-v3.pt", - "ball_detection": "football-ball-detection-v2.pt", - "pitch_detection": "football-pitch-detection-v1.pt", - }, - "confidence": { - "player": 0.25, - "ball": 0.20, - "pitch": 0.25, - }, - "device": "cpu", - "default_detections": { - "players": True, - "ball": True, - "pitch": True, - }, -} +# Global cache for config (loaded once, never reloaded in production) +_CONFIG_CACHE: Dict[str, Any] | None = None + + +class ConfigError(RuntimeError): + """Configuration error for YOLO tracker models.""" + + +def _reset_config_cache_for_tests() -> None: + """Reset config cache for test isolation. + + Tests that modify models.yaml at runtime must call this to reload. + Not needed if tests use fixed config or mocks. + """ + global _CONFIG_CACHE + _CONFIG_CACHE = None def load_model_config(config_path: Path = MODEL_CONFIG_PATH) -> Dict[str, Any]: """Load model configuration from YAML file. + Phase 12 strict governance: + - YAML is required to exist + - All required keys (models, confidence, device) must be present + - No in-code fallbacks or defaults + - Missing or invalid config raises ConfigError immediately + Args: config_path: Path to the models.yaml file. Returns: - Dictionary containing model paths and confidence thresholds. + Dictionary containing model paths, confidence thresholds, device, etc. Raises: - FileNotFoundError: If config file doesn't exist and no default. - yaml.YAMLError: If config file is invalid YAML. + ConfigError: If YAML file missing, invalid, or missing required keys. """ + global _CONFIG_CACHE + if _CONFIG_CACHE is not None: + return _CONFIG_CACHE + if not config_path.exists(): - return DEFAULT_MODEL_CONFIG.copy() + raise ConfigError(f"models.yaml not found at: {config_path}") - with open(config_path, "r") as f: - config = yaml.safe_load(f) + try: + with open(config_path, "r", encoding="utf-8") as f: + config = yaml.safe_load(f) + except Exception as exc: + raise ConfigError(f"Failed to parse models.yaml: {exc}") from exc - if config is None: - return DEFAULT_MODEL_CONFIG.copy() + if config is None or not isinstance(config, dict): + raise ConfigError("models.yaml is empty or not a valid YAML dict") - # Merge with defaults to ensure all keys exist - merged = DEFAULT_MODEL_CONFIG.copy() - if "models" in config: - merged["models"].update(config["models"]) - if "confidence" in config: - merged["confidence"].update(config["confidence"]) - if "device" in config: - merged["device"] = config["device"] - if "default_detections" in config: - merged["default_detections"] = config["default_detections"] + # Validate required top-level keys + required_keys = {"models", "confidence", "device"} + missing = required_keys - set(config.keys()) + if missing: + raise ConfigError(f"models.yaml missing required keys: {missing}") - return merged + # Validate device value + device = config.get("device") + if device not in ("cpu", "cuda"): + raise ConfigError( + f"Invalid device in models.yaml: {device!r} (expected 'cpu' or 'cuda')" + ) + + _CONFIG_CACHE = config + return config def get_model_path(model_key: str) -> str: @@ -84,10 +104,13 @@ def get_model_path(model_key: str) -> str: Model file name (e.g., 'football-player-detection-v3.pt'). Raises: - KeyError: If model_key is not found in config. + ConfigError: If model_key is not found in config. """ config = load_model_config() - return config["models"][model_key] + models = config.get("models", {}) + if model_key not in models: + raise ConfigError(f"models.yaml missing model entry for: {model_key}") + return models[model_key] def get_confidence(task: str) -> float: @@ -100,10 +123,37 @@ def get_confidence(task: str) -> float: Confidence threshold as float between 0 and 1. Raises: - KeyError: If task is not found in config. + ConfigError: If task is not found in config or value is invalid. """ config = load_model_config() - return config["confidence"][task] + confidence = config.get("confidence", {}) + if task not in confidence: + raise ConfigError(f"models.yaml missing confidence entry for: {task}") + value = float(confidence[task]) + if not 0.0 <= value <= 1.0: + raise ConfigError(f"Invalid confidence for {task}: {value} (expected 0.0-1.0)") + return value + + +def get_device() -> str: + """Get configured device for inference. + + Phase 12: Strict governance. + - Device must be present in models.yaml + - Valid values: 'cpu' or 'cuda' + - No fallback; if missing, raises ConfigError + + Returns: + Device string: either 'cpu' or 'cuda' + + Raises: + ConfigError: If device is missing or invalid in models.yaml + """ + config = load_model_config() + device = config.get("device") + if device not in ("cpu", "cuda"): + raise ConfigError(f"Invalid device in models.yaml: {device!r}") + return device def get_default_detections() -> list: @@ -111,11 +161,13 @@ def get_default_detections() -> list: Returns: List of detection types to run (e.g., ['players', 'pitch']). + + Raises: + ConfigError: If default_detections is missing or invalid. """ config = load_model_config() - detections_config = config.get( - "default_detections", - {"players": True, "ball": True, "pitch": True}, - ) + detections_config = config.get("default_detections") + if detections_config is None: + raise ConfigError("models.yaml missing key: default_detections") # Convert dict {name: bool} to list of enabled names return [name for name, enabled in detections_config.items() if enabled] diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/_base_detector.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/_base_detector.py index 1a6716b..9bc4680 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/_base_detector.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/_base_detector.py @@ -66,7 +66,9 @@ def __init__( ValueError: If confidence threshold not in [0.0, 1.0] """ if not 0.0 <= default_confidence <= 1.0: - raise ValueError(f"default_confidence must be in [0.0, 1.0], got {default_confidence}") + raise ValueError( + f"default_confidence must be in [0.0, 1.0], got {default_confidence}" + ) self.detector_name: str = detector_name self.model_name: str = model_name @@ -118,7 +120,8 @@ def get_model(self, device: str = "cpu") -> Any: if model_size_kb < 1: logger.warning( - f"⚠️ Model is a stub ({model_size_kb:.2f} KB)! " "Replace with real model." + f"⚠️ Model is a stub ({model_size_kb:.2f} KB)! " + "Replace with real model." ) self._model = YOLO(self.model_path).to(device=device) @@ -272,7 +275,9 @@ def detect_json_with_annotated_frame( # Create annotated frame model = self.get_model(device=device) - detection_result = model(frame, imgsz=self.imgsz, conf=confidence, verbose=False)[0] + detection_result = model( + frame, imgsz=self.imgsz, conf=confidence, verbose=False + )[0] detections = sv.Detections.from_ultralytics(detection_result) # Build labels if class_names provided @@ -280,7 +285,9 @@ def detect_json_with_annotated_frame( if self.class_names: cls_arr = detections.class_id if cls_arr is not None: - labels = [self.class_names.get(int(cls), f"class_{cls}") for cls in cls_arr] + labels = [ + self.class_names.get(int(cls), f"class_{cls}") for cls in cls_arr + ] # Annotate frame annotated = self._annotate_frame(frame, detections, labels) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/ball_detection.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/ball_detection.py index ffd93ac..9d82a7a 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/ball_detection.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/ball_detection.py @@ -129,7 +129,9 @@ def get_ball_detection_model(device: str = "cpu") -> Any: return BALL_DETECTOR.get_model(device=device) -def run_ball_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> Dict[str, Any]: +def run_ball_detection( + frame: np.ndarray[Any, Any], config: Dict[str, Any] +) -> Dict[str, Any]: """Legacy function for plugin.py compatibility. Delegates to either detect_ball_json or detect_ball_json_with_annotated_frame @@ -150,5 +152,7 @@ def run_ball_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> D include_annotated = config.get("include_annotated", False) if include_annotated: - return detect_ball_json_with_annotated_frame(frame, device=device, confidence=confidence) + return detect_ball_json_with_annotated_frame( + frame, device=device, confidence=confidence + ) return detect_ball_json(frame, device=device, confidence=confidence) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/pitch_detection.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/pitch_detection.py index 92a019d..c508146 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/pitch_detection.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/pitch_detection.py @@ -79,21 +79,26 @@ def detect_pitch_json( keypoints_xy = keypoints_data[0] keypoints_conf = ( result.keypoints.conf.cpu().numpy()[0] - if result.keypoints.conf is not None and result.keypoints.conf.numel() > 0 + if result.keypoints.conf is not None + and result.keypoints.conf.numel() > 0 else None ) for i, (x, y) in enumerate(keypoints_xy): kp: Dict[str, Any] = { "xy": [float(x), float(y)], - "confidence": (float(keypoints_conf[i]) if keypoints_conf is not None else 1.0), + "confidence": ( + float(keypoints_conf[i]) if keypoints_conf is not None else 1.0 + ), "name": CONFIG.keypoint_names.get(i, f"keypoint_{i}"), } keypoint_list.append(kp) # Extract pitch corners pitch_polygon: list[list[float]] = [] - valid_keypoints = [kp for kp in keypoint_list if kp["confidence"] > confidence * 0.5] + valid_keypoints = [ + kp for kp in keypoint_list if kp["confidence"] > confidence * 0.5 + ] if len(valid_keypoints) >= 4: corner_names = [ "bottom_left_corner", @@ -200,7 +205,9 @@ def get_pitch_detection_model(device: str = "cpu") -> Any: return PITCH_DETECTOR.get_model(device=device) -def run_pitch_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> Dict[str, Any]: +def run_pitch_detection( + frame: np.ndarray[Any, Any], config: Dict[str, Any] +) -> Dict[str, Any]: """Legacy function for plugin.py compatibility. Delegates to either detect_pitch_json or detect_pitch_json_with_annotated_frame @@ -221,5 +228,7 @@ def run_pitch_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> include_annotated = config.get("include_annotated", False) if include_annotated: - return detect_pitch_json_with_annotated_frame(frame, device=device, confidence=confidence) + return detect_pitch_json_with_annotated_frame( + frame, device=device, confidence=confidence + ) return detect_pitch_json(frame, device=device, confidence=confidence) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_detection.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_detection.py index 1a988ec..b33fcfe 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_detection.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_detection.py @@ -121,7 +121,9 @@ def get_player_detection_model(device: str = "cpu") -> Any: return PLAYER_DETECTOR.get_model(device=device) -def run_player_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> Dict[str, Any]: +def run_player_detection( + frame: np.ndarray[Any, Any], config: Dict[str, Any] +) -> Dict[str, Any]: """Legacy function for plugin.py compatibility. Delegates to either detect_players_json or detect_players_json_with_annotated_frame @@ -142,5 +144,7 @@ def run_player_detection(frame: np.ndarray[Any, Any], config: Dict[str, Any]) -> include_annotated = config.get("include_annotated", False) if include_annotated: - return detect_players_json_with_annotated_frame(frame, device=device, confidence=confidence) + return detect_players_json_with_annotated_frame( + frame, device=device, confidence=confidence + ) return detect_players_json(frame, device=device, confidence=confidence) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_tracking.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_tracking.py index c0642e1..9567c45 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_tracking.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/player_tracking.py @@ -105,7 +105,9 @@ def track_players_json( xyxy = detections.xyxy[i] conf = float(detections.confidence[i]) cls = int(detections.class_id[i]) - track_id = int(detections.track_id[i]) if detections.track_id is not None else -1 + track_id = ( + int(detections.track_id[i]) if detections.track_id is not None else -1 + ) class_name = CLASS_NAMES.get(cls, f"class_{cls}") @@ -159,7 +161,9 @@ def track_players_json_with_annotated_frame( xyxy = detections.xyxy[i] conf = float(detections.confidence[i]) cls = int(detections.class_id[i]) - track_id = int(detections.track_id[i]) if detections.track_id is not None else -1 + track_id = ( + int(detections.track_id[i]) if detections.track_id is not None else -1 + ) class_name = CLASS_NAMES.get(cls, f"class_{cls}") @@ -181,7 +185,9 @@ def track_players_json_with_annotated_frame( labels = [ f"#{track_id if track_id >= 0 else '?'} {CLASS_NAMES.get(int(cls), f'class_{cls}')}" - for track_id, cls in zip(detections.track_id or [-1] * len(detections), detections.class_id) + for track_id, cls in zip( + detections.track_id or [-1] * len(detections), detections.class_id + ) ] annotated = frame.copy() @@ -203,5 +209,7 @@ def run_player_tracking(frame: np.ndarray, config: Dict[str, Any]) -> Dict[str, include_annotated = config.get("include_annotated", False) if include_annotated: - return track_players_json_with_annotated_frame(frame, device=device, confidence=confidence) + return track_players_json_with_annotated_frame( + frame, device=device, confidence=confidence + ) return track_players_json(frame, device=device, confidence=confidence) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py index 130ffac..740bae7 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/inference/radar.py @@ -170,8 +170,12 @@ def generate_radar_json( valid_kp_indices.append(i) if len(valid_kp_indices) >= 4: - src_pts = np.array([keypoints_xy[i] for i in valid_kp_indices[:4]], dtype=np.float32) - tgt_pts = np.array([CONFIG.vertices[i] for i in valid_kp_indices[:4]], dtype=np.float32) + src_pts = np.array( + [keypoints_xy[i] for i in valid_kp_indices[:4]], dtype=np.float32 + ) + tgt_pts = np.array( + [CONFIG.vertices[i] for i in valid_kp_indices[:4]], dtype=np.float32 + ) try: transformer = get_view_transformer(src_pts, tgt_pts) @@ -249,8 +253,12 @@ def radar_json_with_annotated_frame( valid_kp_indices.append(i) if len(valid_kp_indices) >= 4: - src_pts = np.array([keypoints_xy[i] for i in valid_kp_indices[:4]], dtype=np.float32) - tgt_pts = np.array([CONFIG.vertices[i] for i in valid_kp_indices[:4]], dtype=np.float32) + src_pts = np.array( + [keypoints_xy[i] for i in valid_kp_indices[:4]], dtype=np.float32 + ) + tgt_pts = np.array( + [CONFIG.vertices[i] for i in valid_kp_indices[:4]], dtype=np.float32 + ) try: transformer = get_view_transformer(src_pts, tgt_pts) @@ -298,5 +306,7 @@ def run_radar(frame: np.ndarray, config: Dict[str, Any]) -> Dict[str, Any]: include_annotated = config.get("include_annotated", False) if include_annotated: - return radar_json_with_annotated_frame(frame, device=device, confidence=confidence) + return radar_json_with_annotated_frame( + frame, device=device, confidence=confidence + ) return generate_radar_json(frame, device=device, confidence=confidence) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py index 49f143d..2989e61 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/plugin.py @@ -38,6 +38,7 @@ def on_unload(self) -> None: # pragma: no cover # noqa: B027 pass +from forgesyte_yolo_tracker.configs import get_device, ConfigError from forgesyte_yolo_tracker.inference.ball_detection import ( detect_ball_json, detect_ball_json_with_annotated_frame, @@ -60,6 +61,38 @@ def on_unload(self) -> None: # pragma: no cover # noqa: B027 logger = logging.getLogger(__name__) +# --------------------------------------------------------- +# Device resolution — Phase 12 strict governance +# --------------------------------------------------------- +def _resolve_device(options: Dict[str, Any]) -> str: + """Resolve device for inference. + + Phase 12 governance: + - If options contains 'device' and it's not None, use it + - Else fall back to models.yaml device + - If models.yaml missing or invalid, raise ConfigError + + Args: + options: Options dict from API/server + + Returns: + Device string: 'cpu' or 'cuda' + + Raises: + ConfigError: If device resolution fails + """ + # Explicit device in options takes priority + if "device" in options and options["device"]: + return str(options["device"]) + + # Fall back to config-level device (models.yaml) + try: + return get_device() + except ConfigError as e: + logger.error(f"Device resolution failed: {e}") + raise + + # --------------------------------------------------------- # Image decoding helpers (Phase 12 contract: bytes input) # --------------------------------------------------------- @@ -167,7 +200,9 @@ def _tool_radar( def _tool_player_detection_video( video_path: str, output_path: str, device: str = "cpu" ) -> Dict[str, str]: - from forgesyte_yolo_tracker.video.player_detection_video import run_player_detection_video + from forgesyte_yolo_tracker.video.player_detection_video import ( + run_player_detection_video, + ) run_player_detection_video(video_path, output_path, device=device) return {"status": "success", "output_path": output_path} @@ -176,7 +211,9 @@ def _tool_player_detection_video( def _tool_player_tracking_video( video_path: str, output_path: str, device: str = "cpu" ) -> Dict[str, str]: - from forgesyte_yolo_tracker.video.player_tracking_video import run_player_tracking_video + from forgesyte_yolo_tracker.video.player_tracking_video import ( + run_player_tracking_video, + ) run_player_tracking_video(video_path, output_path, device=device) return {"status": "success", "output_path": output_path} @@ -185,7 +222,9 @@ def _tool_player_tracking_video( def _tool_ball_detection_video( video_path: str, output_path: str, device: str = "cpu" ) -> Dict[str, str]: - from forgesyte_yolo_tracker.video.ball_detection_video import run_ball_detection_video + from forgesyte_yolo_tracker.video.ball_detection_video import ( + run_ball_detection_video, + ) run_ball_detection_video(video_path, output_path, device=device) return {"status": "success", "output_path": output_path} @@ -194,13 +233,17 @@ def _tool_ball_detection_video( def _tool_pitch_detection_video( video_path: str, output_path: str, device: str = "cpu" ) -> Dict[str, str]: - from forgesyte_yolo_tracker.video.pitch_detection_video import run_pitch_detection_video + from forgesyte_yolo_tracker.video.pitch_detection_video import ( + run_pitch_detection_video, + ) run_pitch_detection_video(video_path, output_path, device=device) return {"status": "success", "output_path": output_path} -def _tool_radar_video(video_path: str, output_path: str, device: str = "cpu") -> Dict[str, str]: +def _tool_radar_video( + video_path: str, output_path: str, device: str = "cpu" +) -> Dict[str, str]: from forgesyte_yolo_tracker.video.radar_video import run_radar_video run_radar_video(video_path, output_path, device=device) @@ -328,6 +371,12 @@ class Plugin(BasePlugin): # type: ignore[misc] def run_tool(self, tool_name: str, args: Dict[str, Any]) -> Any: """Execute a tool by name with the given arguments. + Phase 12 governance: + - Device is resolved strictly: options → models.yaml + - If neither provided, raise ConfigError + - Device mismatch with inference modules is acceptable + (inference modules keep device="cpu" default for backward compat) + Args: tool_name: Name of tool to execute (must be from manifest) args: Tool arguments dict @@ -343,12 +392,24 @@ def run_tool(self, tool_name: str, args: Dict[str, Any]) -> Any: handler = self.tools[tool_name]["handler"] + # Resolve device strictly (Phase 12) + try: + device = _resolve_device(args) + except ConfigError as e: + logger.error(f"Device resolution failed: {e}") + return { + "error": "device_resolution_failed", + "message": str(e), + "plugin": "yolo-tracker", + "tool": tool_name, + } + # Video tools use different args if "video" in tool_name: return handler( video_path=args.get("video_path"), output_path=args.get("output_path"), - device=args.get("device", "cpu"), + device=device, ) # Frame tools use image_bytes (Phase 12 contract) @@ -361,7 +422,7 @@ def run_tool(self, tool_name: str, args: Dict[str, Any]) -> Any: return handler( image_bytes=image_bytes, - device=args.get("device", "cpu"), + device=device, annotated=args.get("annotated", False), ) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/team.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/team.py index f310d1b..a5bac31 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/team.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/utils/team.py @@ -20,7 +20,9 @@ SIGLIP_MODEL_PATH = "google/siglip-base-patch16-224" -def create_batches(sequence: Iterable[V], batch_size: int) -> Generator[List[V], None, None]: +def create_batches( + sequence: Iterable[V], batch_size: int +) -> Generator[List[V], None, None]: """ Generate batches from a sequence with a specified batch size. @@ -59,7 +61,9 @@ def __init__(self, device: str = "cpu", batch_size: int = 32) -> None: """ self.device = device self.batch_size = batch_size - self.features_model = SiglipVisionModel.from_pretrained(SIGLIP_MODEL_PATH).to(device) + self.features_model = SiglipVisionModel.from_pretrained(SIGLIP_MODEL_PATH).to( + device + ) self.processor = AutoProcessor.from_pretrained(SIGLIP_MODEL_PATH) if umap is not None: self.reducer = umap.UMAP(n_components=3) @@ -86,7 +90,9 @@ def extract_features(self, crops: List[np.ndarray]) -> np.ndarray: data = [] with torch.no_grad(): for batch in tqdm(batches, desc="Embedding extraction"): - inputs = self.processor(images=batch, return_tensors="pt").to(self.device) + inputs = self.processor(images=batch, return_tensors="pt").to( + self.device + ) outputs = self.features_model(**inputs) embeddings = torch.mean(outputs.last_hidden_state, dim=1).cpu().numpy() data.append(embeddings) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_detection_video.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_detection_video.py index 8fc79f0..b7abaf0 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_detection_video.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_detection_video.py @@ -69,7 +69,9 @@ def run_player_detection_video_frames( result = model(frame, imgsz=1280, conf=confidence, verbose=False)[0] detections = sv.Detections.from_ultralytics(result) - labels = [CLASS_NAMES.get(int(cls), f"class_{cls}") for cls in detections.class_id] + labels = [ + CLASS_NAMES.get(int(cls), f"class_{cls}") for cls in detections.class_id + ] annotated = frame.copy() annotated = box_annotator.annotate(annotated, detections) diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_tracking_video.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_tracking_video.py index 4745ee4..0ddb527 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_tracking_video.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/player_tracking_video.py @@ -69,7 +69,9 @@ def run_player_tracking_video_frames( labels = [ f"#{int(tid) if tid else '?'} {CLASS_NAMES.get(int(cls), f'class_{cls}')}" - for tid, cls in zip(detections.track_id or [-1] * len(detections), detections.class_id) + for tid, cls in zip( + detections.track_id or [-1] * len(detections), detections.class_id + ) ] annotated = frame.copy() diff --git a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/radar_video.py b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/radar_video.py index e0f0364..e9a6d01 100644 --- a/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/radar_video.py +++ b/plugins/forgesyte-yolo-tracker/src/forgesyte_yolo_tracker/video/radar_video.py @@ -98,7 +98,9 @@ def run_radar_video_frames( radar_w, radar_h = CONFIG.radar_resolution for frame in frame_generator: - player_result = player_model(frame, imgsz=1280, conf=confidence, verbose=False)[0] + player_result = player_model(frame, imgsz=1280, conf=confidence, verbose=False)[ + 0 + ] pitch_result = pitch_model(frame, imgsz=1280, conf=confidence, verbose=False)[0] player_detections = sv.Detections.from_ultralytics(player_result) @@ -137,7 +139,9 @@ def run_radar_video_frames( transformed = transformer.transform_points( np.array([[center_x, center_y]], dtype=np.float32) ) - rx, ry = CONFIG.world_to_radar(transformed[0][0], transformed[0][1]) + rx, ry = CONFIG.world_to_radar( + transformed[0][0], transformed[0][1] + ) radar_points.append( { diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/conftest.py b/plugins/forgesyte-yolo-tracker/tests_contract/conftest.py index fcad3fc..d5d4b50 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/conftest.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/conftest.py @@ -33,8 +33,8 @@ def create_mock_module(name: str) -> MagicMock: sys.modules["forgesyte_yolo_tracker.inference.player_detection"] = create_mock_module( "forgesyte_yolo_tracker.inference.player_detection" ) -sys.modules["forgesyte_yolo_tracker.inference.player_detection"].detect_players_json = MagicMock( - return_value={"detections": [], "count": 0} +sys.modules["forgesyte_yolo_tracker.inference.player_detection"].detect_players_json = ( + MagicMock(return_value={"detections": [], "count": 0}) ) sys.modules[ "forgesyte_yolo_tracker.inference.player_detection" @@ -57,8 +57,8 @@ def create_mock_module(name: str) -> MagicMock: sys.modules["forgesyte_yolo_tracker.inference.ball_detection"] = create_mock_module( "forgesyte_yolo_tracker.inference.ball_detection" ) -sys.modules["forgesyte_yolo_tracker.inference.ball_detection"].detect_ball_json = MagicMock( - return_value={"detections": [], "count": 0} +sys.modules["forgesyte_yolo_tracker.inference.ball_detection"].detect_ball_json = ( + MagicMock(return_value={"detections": [], "count": 0}) ) sys.modules[ "forgesyte_yolo_tracker.inference.ball_detection" @@ -69,8 +69,8 @@ def create_mock_module(name: str) -> MagicMock: sys.modules["forgesyte_yolo_tracker.inference.pitch_detection"] = create_mock_module( "forgesyte_yolo_tracker.inference.pitch_detection" ) -sys.modules["forgesyte_yolo_tracker.inference.pitch_detection"].detect_pitch_json = MagicMock( - return_value={"pitch": None} +sys.modules["forgesyte_yolo_tracker.inference.pitch_detection"].detect_pitch_json = ( + MagicMock(return_value={"pitch": None}) ) sys.modules[ "forgesyte_yolo_tracker.inference.pitch_detection" @@ -81,8 +81,8 @@ def create_mock_module(name: str) -> MagicMock: sys.modules["forgesyte_yolo_tracker.inference.player_tracking"] = create_mock_module( "forgesyte_yolo_tracker.inference.player_tracking" ) -sys.modules["forgesyte_yolo_tracker.inference.player_tracking"].track_players_json = MagicMock( - return_value={"tracks": [], "count": 0} +sys.modules["forgesyte_yolo_tracker.inference.player_tracking"].track_players_json = ( + MagicMock(return_value={"tracks": [], "count": 0}) ) sys.modules[ "forgesyte_yolo_tracker.inference.player_tracking" @@ -96,6 +96,8 @@ def create_mock_module(name: str) -> MagicMock: sys.modules["forgesyte_yolo_tracker.inference.radar"].generate_radar_json = MagicMock( return_value={"radar": None} ) -sys.modules["forgesyte_yolo_tracker.inference.radar"].radar_json_with_annotated_frame = MagicMock( +sys.modules[ + "forgesyte_yolo_tracker.inference.radar" +].radar_json_with_annotated_frame = MagicMock( return_value={"radar": None, "annotated_frame": ""} ) diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/constants.py b/plugins/forgesyte-yolo-tracker/tests_contract/constants.py index d3044d5..581af8a 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/constants.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/constants.py @@ -13,7 +13,9 @@ MODELS_DIR = Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "models" # Path to config file -CONFIG_PATH = Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "configs" / "models.yaml" +CONFIG_PATH = ( + Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "configs" / "models.yaml" +) # Load model names from config @@ -43,7 +45,9 @@ def _load_model_names() -> dict: PITCH_MODEL_PATH = MODELS_DIR / PITCH_MODEL # Check if any model exists -MODELS_EXIST = PLAYER_MODEL_PATH.exists() or BALL_MODEL_PATH.exists() or PITCH_MODEL_PATH.exists() +MODELS_EXIST = ( + PLAYER_MODEL_PATH.exists() or BALL_MODEL_PATH.exists() or PITCH_MODEL_PATH.exists() +) # Environment flag RUN_MODEL_TESTS = os.getenv("RUN_MODEL_TESTS", "0") == "1" diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/test_class_mapping.py b/plugins/forgesyte-yolo-tracker/tests_contract/test_class_mapping.py index 815a6b2..8a3247c 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/test_class_mapping.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/test_class_mapping.py @@ -6,16 +6,14 @@ class TestClassMapping: def test_class_names_match_trained_model(self) -> None: """Verify CLASS_NAMES matches trained model's 4-class structure.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - CLASS_NAMES + from forgesyte_yolo_tracker.inference.player_detection import CLASS_NAMES expected = {0: "ball", 1: "goalkeeper", 2: "player", 3: "referee"} assert CLASS_NAMES == expected def test_class_names_has_all_classes(self) -> None: """Verify CLASS_NAMES includes all 4 class IDs.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - CLASS_NAMES + from forgesyte_yolo_tracker.inference.player_detection import CLASS_NAMES assert 0 in CLASS_NAMES # ball assert 1 in CLASS_NAMES # goalkeeper @@ -24,8 +22,7 @@ def test_class_names_has_all_classes(self) -> None: def test_team_colors_has_all_classes(self) -> None: """Verify TEAM_COLORS includes all 4 class IDs.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - TEAM_COLORS + from forgesyte_yolo_tracker.inference.player_detection import TEAM_COLORS assert 0 in TEAM_COLORS assert 1 in TEAM_COLORS @@ -34,8 +31,7 @@ def test_team_colors_has_all_classes(self) -> None: def test_team_colors_are_valid_hex(self) -> None: """Verify TEAM_COLORS are valid hex color strings.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - TEAM_COLORS + from forgesyte_yolo_tracker.inference.player_detection import TEAM_COLORS for class_id, color in TEAM_COLORS.items(): assert isinstance(color, str) diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/test_phase12_input_contract.py b/plugins/forgesyte-yolo-tracker/tests_contract/test_phase12_input_contract.py index 2c1fb2f..890c016 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/test_phase12_input_contract.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/test_phase12_input_contract.py @@ -105,19 +105,17 @@ def test_rejects_string_image_bytes(self, plugin: Plugin) -> None: assert "error" in result assert "invalid_image_bytes" in result["error"] - def test_schema_declares_image_bytes_not_frame_base64( - self, plugin: Plugin - ) -> None: + def test_schema_declares_image_bytes_not_frame_base64(self, plugin: Plugin) -> None: """Verify tool schema declares image_bytes, not frame_base64.""" for tool_name, tool_def in plugin.tools.items(): if "video" not in tool_name: schema = tool_def["input_schema"] - assert "image_bytes" in schema, ( - f"Tool {tool_name} must have 'image_bytes' in schema" - ) - assert "frame_base64" not in schema, ( - f"Tool {tool_name} must NOT have 'frame_base64' in schema" - ) + assert ( + "image_bytes" in schema + ), f"Tool {tool_name} must have 'image_bytes' in schema" + assert ( + "frame_base64" not in schema + ), f"Tool {tool_name} must NOT have 'frame_base64' in schema" def test_no_default_tool_alias(self, plugin: Plugin) -> None: """Verify 'default' tool name is rejected (Phase 12 forbids fallback).""" diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin.py b/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin.py index f9f9ca1..f19c9bd 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin.py @@ -186,7 +186,9 @@ def rgba_frame_base64(self) -> str: img.save(img_bytes, format="PNG") return base64.b64encode(img_bytes.getvalue()).decode("utf-8") - def test_run_tool_handles_rgb_image(self, plugin: Plugin, sample_frame_base64: str) -> None: + def test_run_tool_handles_rgb_image( + self, plugin: Plugin, sample_frame_base64: str + ) -> None: """Test tool handles RGB images correctly.""" with patch( "forgesyte_yolo_tracker.plugin.detect_players_json", @@ -219,7 +221,9 @@ def test_run_tool_handles_grayscale_image( ) assert isinstance(result, dict) - def test_run_tool_handles_rgba_image(self, plugin: Plugin, rgba_frame_base64: str) -> None: + def test_run_tool_handles_rgba_image( + self, plugin: Plugin, rgba_frame_base64: str + ) -> None: """Test tool handles RGBA images (with alpha channel).""" with patch( "forgesyte_yolo_tracker.plugin.detect_players_json", @@ -248,7 +252,9 @@ def test_run_tool_handles_invalid_image_gracefully(self, plugin: Plugin) -> None assert isinstance(result, dict) assert "error" in result - def test_run_tool_respects_options(self, plugin: Plugin, sample_frame_base64: str) -> None: + def test_run_tool_respects_options( + self, plugin: Plugin, sample_frame_base64: str + ) -> None: """Test tool respects options parameter.""" with patch( "forgesyte_yolo_tracker.plugin.detect_players_json_with_annotated_frame", @@ -264,14 +270,18 @@ def test_run_tool_respects_options(self, plugin: Plugin, sample_frame_base64: st ) assert isinstance(result, dict) - def test_run_tool_output_is_json_safe(self, plugin: Plugin, sample_frame_base64: str) -> None: + def test_run_tool_output_is_json_safe( + self, plugin: Plugin, sample_frame_base64: str + ) -> None: """Test tool output is JSON-serializable.""" import json with patch( "forgesyte_yolo_tracker.plugin.detect_players_json", return_value={ - "detections": [{"x1": 100, "y1": 200, "x2": 150, "y2": 350, "confidence": 0.92}] + "detections": [ + {"x1": 100, "y1": 200, "x2": 150, "y2": 350, "confidence": 0.92} + ] }, ): result = plugin.run_tool( diff --git a/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin_edge_cases.py b/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin_edge_cases.py index 516e100..7d7ca0f 100644 --- a/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin_edge_cases.py +++ b/plugins/forgesyte-yolo-tracker/tests_contract/test_plugin_edge_cases.py @@ -70,7 +70,9 @@ def test_base64_with_newlines(self, plugin: Plugin) -> None: b64_data = base64.b64encode(frame_bytes.getvalue()).decode("utf-8") # Add newlines (common in multi-line base64) - b64_with_newlines = "\n".join([b64_data[i : i + 80] for i in range(0, len(b64_data), 80)]) + b64_with_newlines = "\n".join( + [b64_data[i : i + 80] for i in range(0, len(b64_data), 80)] + ) result = plugin.run_tool( "player_detection", @@ -187,7 +189,10 @@ def test_ball_detection_tool_with_annotated( """ with patch( "forgesyte_yolo_tracker.plugin.detect_ball_json_with_annotated_frame", - return_value={"ball": {"x": 320, "y": 240}, "annotated_frame_base64": "iVBOR..."}, + return_value={ + "ball": {"x": 320, "y": 240}, + "annotated_frame_base64": "iVBOR...", + }, ): result = plugin.run_tool( "ball_detection", @@ -254,7 +259,9 @@ def test_pitch_detection_error_handling(self, plugin: Plugin) -> None: assert "error" in result # === RADAR === - def test_radar_tool_no_annotated(self, plugin: Plugin, sample_frame_base64: str) -> None: + def test_radar_tool_no_annotated( + self, plugin: Plugin, sample_frame_base64: str + ) -> None: """Test _tool_radar without annotated frame.""" with patch( "forgesyte_yolo_tracker.plugin.radar_json", @@ -266,7 +273,9 @@ def test_radar_tool_no_annotated(self, plugin: Plugin, sample_frame_base64: str) ) assert isinstance(result, dict) - def test_radar_tool_with_annotated(self, plugin: Plugin, sample_frame_base64: str) -> None: + def test_radar_tool_with_annotated( + self, plugin: Plugin, sample_frame_base64: str + ) -> None: """Test _tool_radar with annotated=True. Triggers lines 149-154: annotated path in _tool_radar @@ -307,7 +316,7 @@ def plugin(self) -> Plugin: def test_error_dict_structure(self, plugin: Plugin) -> None: """Test error dict has required fields (Phase 12 contract). - + When image_bytes is invalid, dispatcher returns error dict. """ result = plugin.run_tool( diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/constants.py b/plugins/forgesyte-yolo-tracker/tests_heavy/constants.py index d3044d5..581af8a 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/constants.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/constants.py @@ -13,7 +13,9 @@ MODELS_DIR = Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "models" # Path to config file -CONFIG_PATH = Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "configs" / "models.yaml" +CONFIG_PATH = ( + Path(__file__).parents[2] / "forgesyte_yolo_tracker" / "configs" / "models.yaml" +) # Load model names from config @@ -43,7 +45,9 @@ def _load_model_names() -> dict: PITCH_MODEL_PATH = MODELS_DIR / PITCH_MODEL # Check if any model exists -MODELS_EXIST = PLAYER_MODEL_PATH.exists() or BALL_MODEL_PATH.exists() or PITCH_MODEL_PATH.exists() +MODELS_EXIST = ( + PLAYER_MODEL_PATH.exists() or BALL_MODEL_PATH.exists() or PITCH_MODEL_PATH.exists() +) # Environment flag RUN_MODEL_TESTS = os.getenv("RUN_MODEL_TESTS", "0") == "1" diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_ball_detection_refactored.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_ball_detection_refactored.py index ecfa0e1..f707cab 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_ball_detection_refactored.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_ball_detection_refactored.py @@ -24,36 +24,31 @@ class TestBallDetectorConfiguration: def test_ball_detector_name_is_correct(self) -> None: """Verify detector name is 'ball'.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - BALL_DETECTOR + from forgesyte_yolo_tracker.inference.ball_detection import BALL_DETECTOR assert BALL_DETECTOR.detector_name == "ball" def test_ball_default_confidence_is_0_20(self) -> None: """Verify default confidence is 0.20.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - BALL_DETECTOR + from forgesyte_yolo_tracker.inference.ball_detection import BALL_DETECTOR assert BALL_DETECTOR.default_confidence == 0.20 def test_ball_imgsz_is_640(self) -> None: """Verify imgsz is 640 for ball.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - BALL_DETECTOR + from forgesyte_yolo_tracker.inference.ball_detection import BALL_DETECTOR assert BALL_DETECTOR.imgsz == 640 def test_ball_class_names_is_none(self) -> None: """Verify class_names is None for ball.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - BALL_DETECTOR + from forgesyte_yolo_tracker.inference.ball_detection import BALL_DETECTOR assert BALL_DETECTOR.class_names is None def test_ball_colors_defined(self) -> None: """Verify colors defined for ball detector.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - BALL_DETECTOR + from forgesyte_yolo_tracker.inference.ball_detection import BALL_DETECTOR assert BALL_DETECTOR.colors is not None assert len(BALL_DETECTOR.colors) > 0 @@ -64,8 +59,7 @@ class TestDetectBallJSON: def test_detect_ball_json_returns_dict(self) -> None: """Verify detect_ball_json returns dictionary.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -74,8 +68,7 @@ def test_detect_ball_json_returns_dict(self) -> None: def test_detect_ball_json_returns_detections_key(self) -> None: """Verify detections key in result.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -85,8 +78,7 @@ def test_detect_ball_json_returns_detections_key(self) -> None: def test_detect_ball_json_returns_count(self) -> None: """Verify count key in result.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -96,8 +88,7 @@ def test_detect_ball_json_returns_count(self) -> None: def test_detect_ball_json_returns_ball_key(self) -> None: """Verify ball key with primary detection.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -106,8 +97,7 @@ def test_detect_ball_json_returns_ball_key(self) -> None: def test_detect_ball_json_returns_ball_detected_boolean(self) -> None: """Verify ball_detected boolean key.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -117,8 +107,7 @@ def test_detect_ball_json_returns_ball_detected_boolean(self) -> None: def test_detect_ball_json_ball_detected_matches_ball_exists(self) -> None: """Verify ball_detected matches if ball exists.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -127,8 +116,7 @@ def test_detect_ball_json_ball_detected_matches_ball_exists(self) -> None: def test_detect_ball_json_count_matches_detections_length(self) -> None: """Verify count matches length of detections list.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -137,8 +125,7 @@ def test_detect_ball_json_count_matches_detections_length(self) -> None: def test_detect_ball_json_detections_have_xyxy(self) -> None: """Verify each detection has xyxy coordinates.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -149,8 +136,7 @@ def test_detect_ball_json_detections_have_xyxy(self) -> None: def test_detect_ball_json_detections_have_confidence(self) -> None: """Verify each detection has confidence score.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -162,8 +148,7 @@ def test_detect_ball_json_detections_have_confidence(self) -> None: def test_detect_ball_json_ball_is_highest_confidence(self) -> None: """Verify ball is highest confidence detection.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -174,8 +159,7 @@ def test_detect_ball_json_ball_is_highest_confidence(self) -> None: def test_detect_ball_json_respects_confidence_parameter(self) -> None: """Verify confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result_low = detect_ball_json(frame, device="cpu", confidence=0.10) @@ -186,8 +170,7 @@ def test_detect_ball_json_respects_confidence_parameter(self) -> None: def test_detect_ball_json_accepts_device_parameter(self) -> None: """Verify device parameter is accepted.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json + from forgesyte_yolo_tracker.inference.ball_detection import detect_ball_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json(frame, device="cpu") @@ -200,8 +183,9 @@ class TestDetectBallJSONWithAnnotated: def test_detect_ball_with_annotated_returns_dict(self) -> None: """Verify returns dictionary.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -210,8 +194,9 @@ def test_detect_ball_with_annotated_returns_dict(self) -> None: def test_detect_ball_with_annotated_includes_detections(self) -> None: """Verify includes detections key.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -221,8 +206,9 @@ def test_detect_ball_with_annotated_includes_detections(self) -> None: def test_detect_ball_with_annotated_includes_count(self) -> None: """Verify includes count key.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -232,8 +218,9 @@ def test_detect_ball_with_annotated_includes_count(self) -> None: def test_detect_ball_with_annotated_includes_ball_detected(self) -> None: """Verify includes ball_detected boolean.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -243,8 +230,9 @@ def test_detect_ball_with_annotated_includes_ball_detected(self) -> None: def test_detect_ball_with_annotated_returns_base64(self) -> None: """Verify returns annotated_frame_base64 key.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -254,8 +242,9 @@ def test_detect_ball_with_annotated_returns_base64(self) -> None: def test_detect_ball_with_annotated_base64_is_valid(self) -> None: """Verify annotated_frame_base64 is valid base64.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -268,8 +257,9 @@ def test_detect_ball_with_annotated_base64_is_valid(self) -> None: def test_detect_ball_with_annotated_respects_device(self) -> None: """Verify respects device parameter.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_ball_json_with_annotated_frame(frame, device="cpu") @@ -278,11 +268,14 @@ def test_detect_ball_with_annotated_respects_device(self) -> None: def test_detect_ball_with_annotated_respects_confidence(self) -> None: """Verify respects confidence parameter.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - detect_ball_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.ball_detection import ( + detect_ball_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) - result = detect_ball_json_with_annotated_frame(frame, device="cpu", confidence=0.50) + result = detect_ball_json_with_annotated_frame( + frame, device="cpu", confidence=0.50 + ) assert result is not None assert "annotated_frame_base64" in result @@ -293,16 +286,18 @@ class TestBallDetectionModelCaching: def test_get_ball_detection_model_returns_instance(self) -> None: """Verify get_ball_detection_model returns model.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - get_ball_detection_model + from forgesyte_yolo_tracker.inference.ball_detection import ( + get_ball_detection_model, + ) model = get_ball_detection_model(device="cpu") assert model is not None def test_get_ball_detection_model_cached(self) -> None: """Verify model is cached after first call.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - get_ball_detection_model + from forgesyte_yolo_tracker.inference.ball_detection import ( + get_ball_detection_model, + ) model1 = get_ball_detection_model(device="cpu") model2 = get_ball_detection_model(device="cpu") @@ -315,8 +310,7 @@ class TestRunBallDetection: def test_run_ball_detection_returns_dict(self) -> None: """Verify run_ball_detection returns dictionary.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - run_ball_detection + from forgesyte_yolo_tracker.inference.ball_detection import run_ball_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -326,8 +320,7 @@ def test_run_ball_detection_returns_dict(self) -> None: def test_run_ball_detection_json_mode(self) -> None: """Verify JSON mode returns detections without base64.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - run_ball_detection + from forgesyte_yolo_tracker.inference.ball_detection import run_ball_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -338,8 +331,7 @@ def test_run_ball_detection_json_mode(self) -> None: def test_run_ball_detection_annotated_mode(self) -> None: """Verify annotated mode includes base64.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - run_ball_detection + from forgesyte_yolo_tracker.inference.ball_detection import run_ball_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": True} @@ -350,8 +342,7 @@ def test_run_ball_detection_annotated_mode(self) -> None: def test_run_ball_detection_respects_config_device(self) -> None: """Verify config device parameter is respected.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - run_ball_detection + from forgesyte_yolo_tracker.inference.ball_detection import run_ball_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu"} @@ -361,8 +352,7 @@ def test_run_ball_detection_respects_config_device(self) -> None: def test_run_ball_detection_respects_config_confidence(self) -> None: """Verify config confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - run_ball_detection + from forgesyte_yolo_tracker.inference.ball_detection import run_ball_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "confidence": 0.30} diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_pitch_detection_refactored.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_pitch_detection_refactored.py index 4c83099..cc989fd 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_pitch_detection_refactored.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_pitch_detection_refactored.py @@ -24,36 +24,31 @@ class TestPitchDetectorConfiguration: def test_pitch_detector_name_is_correct(self) -> None: """Verify detector name is 'pitch'.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - PITCH_DETECTOR + from forgesyte_yolo_tracker.inference.pitch_detection import PITCH_DETECTOR assert PITCH_DETECTOR.detector_name == "pitch" def test_pitch_default_confidence_is_0_25(self) -> None: """Verify default confidence is 0.25.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - PITCH_DETECTOR + from forgesyte_yolo_tracker.inference.pitch_detection import PITCH_DETECTOR assert PITCH_DETECTOR.default_confidence == 0.25 def test_pitch_imgsz_is_1280(self) -> None: """Verify imgsz is 1280 for pitch.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - PITCH_DETECTOR + from forgesyte_yolo_tracker.inference.pitch_detection import PITCH_DETECTOR assert PITCH_DETECTOR.imgsz == 1280 def test_pitch_class_names_is_none(self) -> None: """Verify class_names is None (uses keypoints).""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - PITCH_DETECTOR + from forgesyte_yolo_tracker.inference.pitch_detection import PITCH_DETECTOR assert PITCH_DETECTOR.class_names is None def test_pitch_colors_defined(self) -> None: """Verify colors defined for pitch detector.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - PITCH_DETECTOR + from forgesyte_yolo_tracker.inference.pitch_detection import PITCH_DETECTOR assert PITCH_DETECTOR.colors is not None @@ -63,8 +58,7 @@ class TestDetectPitchJSON: def test_detect_pitch_json_returns_dict(self) -> None: """Verify detect_pitch_json returns dictionary.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -73,8 +67,7 @@ def test_detect_pitch_json_returns_dict(self) -> None: def test_detect_pitch_json_returns_keypoints_key(self) -> None: """Verify keypoints key in result.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -84,8 +77,7 @@ def test_detect_pitch_json_returns_keypoints_key(self) -> None: def test_detect_pitch_json_returns_count(self) -> None: """Verify count key in result.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -95,8 +87,7 @@ def test_detect_pitch_json_returns_count(self) -> None: def test_detect_pitch_json_returns_pitch_polygon(self) -> None: """Verify pitch_polygon key in result.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -106,8 +97,7 @@ def test_detect_pitch_json_returns_pitch_polygon(self) -> None: def test_detect_pitch_json_returns_pitch_detected_boolean(self) -> None: """Verify pitch_detected boolean key.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -117,8 +107,7 @@ def test_detect_pitch_json_returns_pitch_detected_boolean(self) -> None: def test_detect_pitch_json_returns_homography(self) -> None: """Verify homography key in result.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -127,8 +116,7 @@ def test_detect_pitch_json_returns_homography(self) -> None: def test_detect_pitch_json_pitch_detected_true_when_4_corners(self) -> None: """Verify pitch_detected is true when >= 4 corners.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -137,8 +125,7 @@ def test_detect_pitch_json_pitch_detected_true_when_4_corners(self) -> None: def test_detect_pitch_json_keypoints_have_xy(self) -> None: """Verify each keypoint has xy coordinates.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -149,8 +136,7 @@ def test_detect_pitch_json_keypoints_have_xy(self) -> None: def test_detect_pitch_json_keypoints_have_confidence(self) -> None: """Verify each keypoint has confidence.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -161,8 +147,7 @@ def test_detect_pitch_json_keypoints_have_confidence(self) -> None: def test_detect_pitch_json_keypoints_have_name(self) -> None: """Verify each keypoint has name.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -173,8 +158,7 @@ def test_detect_pitch_json_keypoints_have_name(self) -> None: def test_detect_pitch_json_count_matches_keypoints_length(self) -> None: """Verify count matches length of keypoints list.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -183,8 +167,7 @@ def test_detect_pitch_json_count_matches_keypoints_length(self) -> None: def test_detect_pitch_json_respects_confidence_parameter(self) -> None: """Verify confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result_low = detect_pitch_json(frame, device="cpu", confidence=0.10) @@ -195,8 +178,7 @@ def test_detect_pitch_json_respects_confidence_parameter(self) -> None: def test_detect_pitch_json_accepts_device_parameter(self) -> None: """Verify device parameter is accepted.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json + from forgesyte_yolo_tracker.inference.pitch_detection import detect_pitch_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json(frame, device="cpu") @@ -209,8 +191,9 @@ class TestDetectPitchJSONWithAnnotated: def test_detect_pitch_with_annotated_returns_dict(self) -> None: """Verify returns dictionary.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -219,8 +202,9 @@ def test_detect_pitch_with_annotated_returns_dict(self) -> None: def test_detect_pitch_with_annotated_includes_keypoints(self) -> None: """Verify includes keypoints key.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -230,8 +214,9 @@ def test_detect_pitch_with_annotated_includes_keypoints(self) -> None: def test_detect_pitch_with_annotated_includes_count(self) -> None: """Verify includes count key.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -241,8 +226,9 @@ def test_detect_pitch_with_annotated_includes_count(self) -> None: def test_detect_pitch_with_annotated_includes_pitch_detected(self) -> None: """Verify includes pitch_detected boolean.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -252,8 +238,9 @@ def test_detect_pitch_with_annotated_includes_pitch_detected(self) -> None: def test_detect_pitch_with_annotated_returns_base64(self) -> None: """Verify returns annotated_frame_base64 key.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -263,8 +250,9 @@ def test_detect_pitch_with_annotated_returns_base64(self) -> None: def test_detect_pitch_with_annotated_base64_is_valid(self) -> None: """Verify annotated_frame_base64 is valid base64.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -277,8 +265,9 @@ def test_detect_pitch_with_annotated_base64_is_valid(self) -> None: def test_detect_pitch_with_annotated_respects_device(self) -> None: """Verify respects device parameter.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_pitch_json_with_annotated_frame(frame, device="cpu") @@ -287,11 +276,14 @@ def test_detect_pitch_with_annotated_respects_device(self) -> None: def test_detect_pitch_with_annotated_respects_confidence(self) -> None: """Verify respects confidence parameter.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - detect_pitch_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.pitch_detection import ( + detect_pitch_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) - result = detect_pitch_json_with_annotated_frame(frame, device="cpu", confidence=0.50) + result = detect_pitch_json_with_annotated_frame( + frame, device="cpu", confidence=0.50 + ) assert result is not None assert "annotated_frame_base64" in result @@ -302,16 +294,18 @@ class TestPitchDetectionModelCaching: def test_get_pitch_detection_model_returns_instance(self) -> None: """Verify get_pitch_detection_model returns model.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - get_pitch_detection_model + from forgesyte_yolo_tracker.inference.pitch_detection import ( + get_pitch_detection_model, + ) model = get_pitch_detection_model(device="cpu") assert model is not None def test_get_pitch_detection_model_cached(self) -> None: """Verify model is cached after first call.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - get_pitch_detection_model + from forgesyte_yolo_tracker.inference.pitch_detection import ( + get_pitch_detection_model, + ) model1 = get_pitch_detection_model(device="cpu") model2 = get_pitch_detection_model(device="cpu") @@ -324,8 +318,7 @@ class TestRunPitchDetection: def test_run_pitch_detection_returns_dict(self) -> None: """Verify run_pitch_detection returns dictionary.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - run_pitch_detection + from forgesyte_yolo_tracker.inference.pitch_detection import run_pitch_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -335,8 +328,7 @@ def test_run_pitch_detection_returns_dict(self) -> None: def test_run_pitch_detection_json_mode(self) -> None: """Verify JSON mode returns keypoints without base64.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - run_pitch_detection + from forgesyte_yolo_tracker.inference.pitch_detection import run_pitch_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -347,8 +339,7 @@ def test_run_pitch_detection_json_mode(self) -> None: def test_run_pitch_detection_annotated_mode(self) -> None: """Verify annotated mode includes base64.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - run_pitch_detection + from forgesyte_yolo_tracker.inference.pitch_detection import run_pitch_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": True} @@ -359,8 +350,7 @@ def test_run_pitch_detection_annotated_mode(self) -> None: def test_run_pitch_detection_respects_config_device(self) -> None: """Verify config device parameter is respected.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - run_pitch_detection + from forgesyte_yolo_tracker.inference.pitch_detection import run_pitch_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu"} @@ -370,8 +360,7 @@ def test_run_pitch_detection_respects_config_device(self) -> None: def test_run_pitch_detection_respects_config_confidence(self) -> None: """Verify config confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.pitch_detection import \ - run_pitch_detection + from forgesyte_yolo_tracker.inference.pitch_detection import run_pitch_detection frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "confidence": 0.30} diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_detection_refactored.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_detection_refactored.py index 72acd35..06fe47f 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_detection_refactored.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_detection_refactored.py @@ -24,69 +24,60 @@ class TestPlayerDetectorConfiguration: def test_player_detector_name_is_correct(self) -> None: """Verify detector name is 'player'.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.detector_name == "player" def test_player_default_confidence_is_0_25(self) -> None: """Verify default confidence is 0.25.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.default_confidence == 0.25 def test_player_imgsz_is_1280(self) -> None: """Verify imgsz is 1280 for players.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.imgsz == 1280 def test_player_class_names_has_4_classes(self) -> None: """Verify 4 class names defined.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.class_names is not None assert len(PLAYER_DETECTOR.class_names) == 4 def test_player_class_names_includes_ball(self) -> None: """Verify 'ball' in class names.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.class_names is not None assert "ball" in PLAYER_DETECTOR.class_names.values() def test_player_class_names_includes_goalkeeper(self) -> None: """Verify 'goalkeeper' in class names.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.class_names is not None assert "goalkeeper" in PLAYER_DETECTOR.class_names.values() def test_player_class_names_includes_player(self) -> None: """Verify 'player' in class names.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.class_names is not None assert "player" in PLAYER_DETECTOR.class_names.values() def test_player_class_names_includes_referee(self) -> None: """Verify 'referee' in class names.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.class_names is not None assert "referee" in PLAYER_DETECTOR.class_names.values() def test_player_colors_defined(self) -> None: """Verify colors defined for player detector.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - PLAYER_DETECTOR + from forgesyte_yolo_tracker.inference.player_detection import PLAYER_DETECTOR assert PLAYER_DETECTOR.colors is not None assert len(PLAYER_DETECTOR.colors) > 0 @@ -97,8 +88,9 @@ class TestDetectPlayersJSON: def test_detect_players_json_returns_dict(self) -> None: """Verify detect_players_json returns dictionary.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -107,8 +99,9 @@ def test_detect_players_json_returns_dict(self) -> None: def test_detect_players_json_returns_detections_key(self) -> None: """Verify detections key in result.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -118,8 +111,9 @@ def test_detect_players_json_returns_detections_key(self) -> None: def test_detect_players_json_returns_count(self) -> None: """Verify count key in result.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -129,8 +123,9 @@ def test_detect_players_json_returns_count(self) -> None: def test_detect_players_json_returns_classes(self) -> None: """Verify classes key in result.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -140,8 +135,9 @@ def test_detect_players_json_returns_classes(self) -> None: def test_detect_players_json_classes_has_all_4_keys(self) -> None: """Verify classes dict has all 4 class names.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -154,8 +150,9 @@ def test_detect_players_json_classes_has_all_4_keys(self) -> None: def test_detect_players_json_count_matches_detections_length(self) -> None: """Verify count matches length of detections list.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -164,8 +161,9 @@ def test_detect_players_json_count_matches_detections_length(self) -> None: def test_detect_players_json_detections_have_xyxy(self) -> None: """Verify each detection has xyxy coordinates.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -176,8 +174,9 @@ def test_detect_players_json_detections_have_xyxy(self) -> None: def test_detect_players_json_detections_have_confidence(self) -> None: """Verify each detection has confidence score.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -189,8 +188,9 @@ def test_detect_players_json_detections_have_confidence(self) -> None: def test_detect_players_json_detections_have_class_name(self) -> None: """Verify each detection has class_name.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -201,8 +201,9 @@ def test_detect_players_json_detections_have_class_name(self) -> None: def test_detect_players_json_respects_confidence_parameter(self) -> None: """Verify confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result_low = detect_players_json(frame, device="cpu", confidence=0.10) @@ -213,8 +214,9 @@ def test_detect_players_json_respects_confidence_parameter(self) -> None: def test_detect_players_json_accepts_device_parameter(self) -> None: """Verify device parameter is accepted.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json(frame, device="cpu") @@ -227,8 +229,9 @@ class TestDetectPlayersJSONWithAnnotated: def test_detect_players_with_annotated_returns_dict(self) -> None: """Verify returns dictionary.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -237,8 +240,9 @@ def test_detect_players_with_annotated_returns_dict(self) -> None: def test_detect_players_with_annotated_includes_detections(self) -> None: """Verify includes detections key.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -248,8 +252,9 @@ def test_detect_players_with_annotated_includes_detections(self) -> None: def test_detect_players_with_annotated_includes_count(self) -> None: """Verify includes count key.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -259,8 +264,9 @@ def test_detect_players_with_annotated_includes_count(self) -> None: def test_detect_players_with_annotated_includes_classes(self) -> None: """Verify includes classes dict.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -270,8 +276,9 @@ def test_detect_players_with_annotated_includes_classes(self) -> None: def test_detect_players_with_annotated_returns_base64(self) -> None: """Verify returns annotated_frame_base64 key.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -281,8 +288,9 @@ def test_detect_players_with_annotated_returns_base64(self) -> None: def test_detect_players_with_annotated_base64_is_valid(self) -> None: """Verify annotated_frame_base64 is valid base64.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -295,8 +303,9 @@ def test_detect_players_with_annotated_base64_is_valid(self) -> None: def test_detect_players_with_annotated_respects_device(self) -> None: """Verify respects device parameter.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = detect_players_json_with_annotated_frame(frame, device="cpu") @@ -305,11 +314,14 @@ def test_detect_players_with_annotated_respects_device(self) -> None: def test_detect_players_with_annotated_respects_confidence(self) -> None: """Verify respects confidence parameter.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - detect_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_detection import ( + detect_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) - result = detect_players_json_with_annotated_frame(frame, device="cpu", confidence=0.50) + result = detect_players_json_with_annotated_frame( + frame, device="cpu", confidence=0.50 + ) assert result is not None assert "annotated_frame_base64" in result @@ -320,16 +332,18 @@ class TestPlayerDetectionModelCaching: def test_get_player_detection_model_returns_instance(self) -> None: """Verify get_player_detection_model returns model.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - get_player_detection_model + from forgesyte_yolo_tracker.inference.player_detection import ( + get_player_detection_model, + ) model = get_player_detection_model(device="cpu") assert model is not None def test_get_player_detection_model_cached(self) -> None: """Verify model is cached after first call.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - get_player_detection_model + from forgesyte_yolo_tracker.inference.player_detection import ( + get_player_detection_model, + ) model1 = get_player_detection_model(device="cpu") model2 = get_player_detection_model(device="cpu") @@ -342,8 +356,9 @@ class TestRunPlayerDetection: def test_run_player_detection_returns_dict(self) -> None: """Verify run_player_detection returns dictionary.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - run_player_detection + from forgesyte_yolo_tracker.inference.player_detection import ( + run_player_detection, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -353,8 +368,9 @@ def test_run_player_detection_returns_dict(self) -> None: def test_run_player_detection_json_mode(self) -> None: """Verify JSON mode returns detections without base64.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - run_player_detection + from forgesyte_yolo_tracker.inference.player_detection import ( + run_player_detection, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": False} @@ -365,8 +381,9 @@ def test_run_player_detection_json_mode(self) -> None: def test_run_player_detection_annotated_mode(self) -> None: """Verify annotated mode includes base64.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - run_player_detection + from forgesyte_yolo_tracker.inference.player_detection import ( + run_player_detection, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "include_annotated": True} @@ -377,8 +394,9 @@ def test_run_player_detection_annotated_mode(self) -> None: def test_run_player_detection_respects_config_device(self) -> None: """Verify config device parameter is respected.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - run_player_detection + from forgesyte_yolo_tracker.inference.player_detection import ( + run_player_detection, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu"} @@ -388,8 +406,9 @@ def test_run_player_detection_respects_config_device(self) -> None: def test_run_player_detection_respects_config_confidence(self) -> None: """Verify config confidence parameter is respected.""" - from forgesyte_yolo_tracker.inference.player_detection import \ - run_player_detection + from forgesyte_yolo_tracker.inference.player_detection import ( + run_player_detection, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) config: Dict[str, Any] = {"device": "cpu", "confidence": 0.50} diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking.py index 3d43cfb..5099a72 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking.py @@ -16,8 +16,7 @@ class TestPlayerTrackingJSON: def test_returns_dict_with_detections(self) -> None: """Verify returns dictionary with detections key.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json(frame, device="cpu") @@ -27,8 +26,7 @@ def test_returns_dict_with_detections(self) -> None: def test_returns_count(self) -> None: """Verify returns count of tracked players.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json(frame, device="cpu") @@ -38,8 +36,7 @@ def test_returns_count(self) -> None: def test_detections_have_tracking_id(self) -> None: """Verify each detection has tracking_id.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json(frame, device="cpu") @@ -50,8 +47,7 @@ def test_detections_have_tracking_id(self) -> None: def test_detections_have_xyxy(self) -> None: """Verify each detection has xyxy coordinates.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json(frame, device="cpu") @@ -66,8 +62,9 @@ class TestPlayerTrackingJSONWithAnnotated: def test_returns_annotated_frame_base64(self) -> None: """Verify returns base64 encoded annotated frame.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_tracking import ( + track_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json_with_annotated_frame(frame, device="cpu") @@ -79,8 +76,9 @@ def test_annotated_frame_includes_tracking_labels(self) -> None: """Verify annotated frame includes tracking ID labels.""" import base64 - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_tracking import ( + track_players_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = track_players_json_with_annotated_frame(frame, device="cpu") diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking_refactored.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking_refactored.py index c784e28..8821e02 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking_refactored.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_player_tracking_refactored.py @@ -48,8 +48,7 @@ class TestTrackIDAssignment: def test_track_id_assignment_single_player(self, sample_frame: np.ndarray) -> None: """Verify track ID assigned to single player.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") @@ -61,8 +60,7 @@ def test_track_id_assignment_single_player(self, sample_frame: np.ndarray) -> No def test_track_id_uniqueness_multi_player(self, sample_frame: np.ndarray) -> None: """Verify each player gets unique track ID.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -75,8 +73,7 @@ def test_track_id_uniqueness_multi_player(self, sample_frame: np.ndarray) -> Non def test_track_id_format_is_integer(self, sample_frame: np.ndarray) -> None: """Verify tracking_id field is integer.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -87,8 +84,7 @@ def test_track_id_format_is_integer(self, sample_frame: np.ndarray) -> None: def test_track_ids_list_contains_valid_ids(self, sample_frame: np.ndarray) -> None: """Verify track_ids list contains only valid IDs.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") track_ids = result["track_ids"] @@ -108,8 +104,7 @@ class TestTrackPersistence: def test_track_persistence_basic(self, sample_frame: np.ndarray) -> None: """Verify same frame returns consistent results.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json # Run same frame twice result1 = track_players_json(sample_frame, device="cpu") @@ -121,8 +116,7 @@ def test_track_persistence_basic(self, sample_frame: np.ndarray) -> None: def test_track_persistence_structure(self, sample_frame: np.ndarray) -> None: """Verify detection structure is consistent.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -136,8 +130,7 @@ def test_track_persistence_structure(self, sample_frame: np.ndarray) -> None: def test_xyxy_format_is_list_of_4(self, sample_frame: np.ndarray) -> None: """Verify xyxy format is [x1, y1, x2, y2].""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -150,8 +143,7 @@ def test_xyxy_format_is_list_of_4(self, sample_frame: np.ndarray) -> None: def test_confidence_in_valid_range(self, sample_frame: np.ndarray) -> None: """Verify confidence values are in [0, 1].""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -172,8 +164,7 @@ class TestOcclusionHandling: def test_partial_detection_confidence_lower(self, sample_frame: np.ndarray) -> None: """Verify partially visible player has valid confidence.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -185,8 +176,7 @@ def test_partial_detection_confidence_lower(self, sample_frame: np.ndarray) -> N def test_multiple_detections_handled(self, sample_frame: np.ndarray) -> None: """Verify multiple detections don't crash.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") @@ -194,10 +184,11 @@ def test_multiple_detections_handled(self, sample_frame: np.ndarray) -> None: assert isinstance(result["count"], int) assert result["count"] >= 0 - def test_no_detections_returns_valid_response(self, sample_frame: np.ndarray) -> None: + def test_no_detections_returns_valid_response( + self, sample_frame: np.ndarray + ) -> None: """Verify empty detections returns valid response.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json # Create empty frame empty_frame = np.zeros((480, 640, 3), dtype=np.uint8) @@ -218,8 +209,7 @@ class TestTrackJSONOutput: def test_json_response_structure(self, sample_frame: np.ndarray) -> None: """Verify JSON has required top-level keys.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") @@ -232,8 +222,7 @@ def test_json_serializable(self, sample_frame: np.ndarray) -> None: """Verify result is JSON serializable.""" import json - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") @@ -255,8 +244,9 @@ class TestAnnotatedFrameOutput: def test_annotated_frame_returns_base64(self, sample_frame: np.ndarray) -> None: """Verify annotated frame output includes base64.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_tracking import ( + track_players_json_with_annotated_frame, + ) result = track_players_json_with_annotated_frame(sample_frame, device="cpu") @@ -268,8 +258,9 @@ def test_annotated_frame_base64_valid(self, sample_frame: np.ndarray) -> None: """Verify base64 string is valid.""" import base64 - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.player_tracking import ( + track_players_json_with_annotated_frame, + ) result = track_players_json_with_annotated_frame(sample_frame, device="cpu") b64_str = result["annotated_frame_base64"] @@ -291,8 +282,7 @@ class TestConfidenceFiltering: def test_confidence_parameter_accepted(self, sample_frame: np.ndarray) -> None: """Verify confidence parameter is accepted.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json # Should accept confidence parameter result = track_players_json(sample_frame, device="cpu", confidence=0.5) @@ -302,8 +292,7 @@ def test_confidence_parameter_accepted(self, sample_frame: np.ndarray) -> None: def test_high_confidence_threshold(self, sample_frame: np.ndarray) -> None: """Verify high confidence threshold reduces detections.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json # Low threshold - should get more detections result_low = track_players_json(sample_frame, device="cpu", confidence=0.1) @@ -324,8 +313,7 @@ class TestDeviceParameter: def test_device_cpu_accepted(self, sample_frame: np.ndarray) -> None: """Verify device='cpu' parameter works.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") @@ -334,8 +322,7 @@ def test_device_cpu_accepted(self, sample_frame: np.ndarray) -> None: def test_device_cuda_accepted(self, sample_frame: np.ndarray) -> None: """Verify device='cuda' parameter accepted (may fall back to cpu).""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json # Should accept cuda parameter (may use cpu if not available) try: @@ -356,8 +343,7 @@ class TestClassNames: def test_valid_class_names(self, sample_frame: np.ndarray) -> None: """Verify class names are valid.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -368,8 +354,7 @@ def test_valid_class_names(self, sample_frame: np.ndarray) -> None: def test_class_id_matches_class_name(self, sample_frame: np.ndarray) -> None: """Verify class_id and class_name correspond.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") detections = result["detections"] @@ -391,8 +376,7 @@ class TestCountConsistency: def test_count_matches_detections_length(self, sample_frame: np.ndarray) -> None: """Verify count matches length of detections list.""" - from forgesyte_yolo_tracker.inference.player_tracking import \ - track_players_json + from forgesyte_yolo_tracker.inference.player_tracking import track_players_json result = track_players_json(sample_frame, device="cpu") diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar.py index e72296d..2be492a 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar.py @@ -43,8 +43,9 @@ class TestRadarJSONWithAnnotated: def test_returns_radar_base64(self) -> None: """Verify returns base64 encoded radar image.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) frame = np.zeros((480, 640, 3), dtype=np.uint8) result = radar_json_with_annotated_frame(frame, device="cpu") diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar_refactored.py b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar_refactored.py index ce9bf58..3803d87 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar_refactored.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/inference/test_inference_radar_refactored.py @@ -170,8 +170,9 @@ class TestAnnotatedRadarFrame: def test_annotated_radar_returns_base64(self, sample_frame: np.ndarray) -> None: """Verify annotated radar includes base64.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") @@ -181,8 +182,9 @@ def test_annotated_radar_returns_base64(self, sample_frame: np.ndarray) -> None: def test_annotated_radar_base64_valid(self, sample_frame: np.ndarray) -> None: """Verify base64 string is valid.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") b64_str = result["radar_base64"] @@ -207,8 +209,9 @@ def test_base64_decodes_to_png(self, sample_frame: np.ndarray) -> None: if Image is None: pytest.skip("PIL not available") - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") b64_str = result["radar_base64"] @@ -222,8 +225,9 @@ def test_base64_decodes_to_png(self, sample_frame: np.ndarray) -> None: def test_base64_string_not_empty(self, sample_frame: np.ndarray) -> None: """Verify base64 string has content.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") b64_str = result["radar_base64"] @@ -232,8 +236,9 @@ def test_base64_string_not_empty(self, sample_frame: np.ndarray) -> None: def test_base64_decode_length(self, sample_frame: np.ndarray) -> None: """Verify decoded base64 has reasonable size.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") b64_str = result["radar_base64"] @@ -330,8 +335,9 @@ def test_annotated_radar_json_serializable(self, sample_frame: np.ndarray) -> No """Verify annotated radar JSON is serializable.""" import json - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) result = radar_json_with_annotated_frame(sample_frame, device="cpu") @@ -408,8 +414,9 @@ def test_empty_frame_returns_valid_response(self) -> None: def test_empty_frame_with_annotated(self) -> None: """Verify empty frame returns annotated frame.""" - from forgesyte_yolo_tracker.inference.radar import \ - radar_json_with_annotated_frame + from forgesyte_yolo_tracker.inference.radar import ( + radar_json_with_annotated_frame, + ) empty_frame = np.zeros((480, 640, 3), dtype=np.uint8) result = radar_json_with_annotated_frame(empty_frame, device="cpu") diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/integration/test_team_integration.py b/plugins/forgesyte-yolo-tracker/tests_heavy/integration/test_team_integration.py index 6342acf..bdd9b85 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/integration/test_team_integration.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/integration/test_team_integration.py @@ -51,7 +51,9 @@ def test_full_fit_predict_pipeline(self) -> None: def test_batch_inference_performance(self) -> None: """Test batch processing with real model.""" classifier = TeamClassifier(device="cpu", batch_size=8) - crops = [np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8) for _ in range(10)] + crops = [ + np.random.randint(0, 255, (224, 224, 3), dtype=np.uint8) for _ in range(10) + ] embeddings = classifier.extract_features(crops) assert embeddings.shape[0] == 10 diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_models.py b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_models.py index feb56f2..3ee39bc 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_models.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_models.py @@ -4,8 +4,12 @@ import pytest -from forgesyte_yolo_tracker.configs import (MODEL_CONFIG_PATH, get_confidence, - get_model_path, load_model_config) +from forgesyte_yolo_tracker.configs import ( + MODEL_CONFIG_PATH, + get_confidence, + get_model_path, + load_model_config, +) class TestLoadModelConfig: diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_soccer.py b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_soccer.py index 5387daa..1c56e6c 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_soccer.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_config_soccer.py @@ -6,24 +6,21 @@ class TestSoccerPitchConfiguration: def test_config_has_vertices(self) -> None: """Verify config has vertices attribute.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert hasattr(config, "vertices") def test_config_has_edges(self) -> None: """Verify config has edges attribute.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert hasattr(config, "edges") def test_config_has_dimensions(self) -> None: """Verify config has width and length.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert hasattr(config, "width") @@ -33,16 +30,14 @@ def test_config_has_dimensions(self) -> None: def test_vertices_count(self) -> None: """Verify we have expected number of keypoints.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert len(config.vertices) >= 14 # Standard pitch has 14+ keypoints def test_edges_form_complete_graph(self) -> None: """Verify edges connect vertices properly.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert len(config.edges) > 0 @@ -54,8 +49,7 @@ def test_edges_form_complete_graph(self) -> None: def test_vertices_are_tuples(self) -> None: """Verify vertices are (x, y) coordinate tuples.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() for vertex in config.vertices: @@ -66,8 +60,7 @@ def test_vertices_are_tuples(self) -> None: def test_pitch_aspect_ratio(self) -> None: """Verify pitch has correct dimensions.""" - from forgesyte_yolo_tracker.configs.soccer import \ - SoccerPitchConfiguration + from forgesyte_yolo_tracker.configs.soccer import SoccerPitchConfiguration config = SoccerPitchConfiguration() assert config.length == 12000 diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_models_directory_structure.py b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_models_directory_structure.py index 2fa5316..4cf706f 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_models_directory_structure.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/legacy/test_models_directory_structure.py @@ -19,7 +19,8 @@ def test_models_dir_exists_at_correct_location(self) -> None: expected_models_dir = config_path.parent.parent / "models" assert expected_models_dir.exists(), ( - f"Models directory should exist at {expected_models_dir}, " f"not at src/models/" + f"Models directory should exist at {expected_models_dir}, " + f"not at src/models/" ) def test_models_directory_is_sibling_of_configs(self) -> None: @@ -36,18 +37,20 @@ def test_models_directory_is_sibling_of_configs(self) -> None: def test_all_inference_modules_use_correct_models_path(self) -> None: """Verify all inference modules resolve models path correctly.""" - from forgesyte_yolo_tracker.inference.ball_detection import \ - MODEL_PATH as bd_path - from forgesyte_yolo_tracker.inference.pitch_detection import \ - MODEL_PATH as pit_path - from forgesyte_yolo_tracker.inference.player_detection import \ - MODEL_PATH as pd_path - from forgesyte_yolo_tracker.inference.player_tracking import \ - MODEL_PATH as pt_path - from forgesyte_yolo_tracker.inference.radar import \ - PITCH_MODEL_PATH as r_pitch - from forgesyte_yolo_tracker.inference.radar import \ - PLAYER_MODEL_PATH as r_player + from forgesyte_yolo_tracker.inference.ball_detection import ( + MODEL_PATH as bd_path, + ) + from forgesyte_yolo_tracker.inference.pitch_detection import ( + MODEL_PATH as pit_path, + ) + from forgesyte_yolo_tracker.inference.player_detection import ( + MODEL_PATH as pd_path, + ) + from forgesyte_yolo_tracker.inference.player_tracking import ( + MODEL_PATH as pt_path, + ) + from forgesyte_yolo_tracker.inference.radar import PITCH_MODEL_PATH as r_pitch + from forgesyte_yolo_tracker.inference.radar import PLAYER_MODEL_PATH as r_player # All should contain "forgesyte_yolo_tracker/models" paths = [pd_path, pt_path, bd_path, pit_path, r_player, r_pitch] @@ -57,22 +60,28 @@ def test_all_inference_modules_use_correct_models_path(self) -> None: ), f"Path should contain 'forgesyte_yolo_tracker': {path}" assert "models" in path, f"Path should contain 'models': {path}" # Ensure it's not using wrong src/models path - assert "/src/models" not in path, f"Path incorrectly points to src/models: {path}" + assert ( + "/src/models" not in path + ), f"Path incorrectly points to src/models: {path}" def test_all_video_modules_use_correct_models_path(self) -> None: """Verify all video modules resolve models path correctly.""" - from forgesyte_yolo_tracker.video.ball_detection_video import \ - MODEL_PATH as bd_path - from forgesyte_yolo_tracker.video.pitch_detection_video import \ - MODEL_PATH as pit_path - from forgesyte_yolo_tracker.video.player_detection_video import \ - MODEL_PATH as pd_path - from forgesyte_yolo_tracker.video.player_tracking_video import \ - MODEL_PATH as pt_path - from forgesyte_yolo_tracker.video.radar_video import \ - PITCH_MODEL_PATH as r_pitch - from forgesyte_yolo_tracker.video.radar_video import \ - PLAYER_MODEL_PATH as r_player + from forgesyte_yolo_tracker.video.ball_detection_video import ( + MODEL_PATH as bd_path, + ) + from forgesyte_yolo_tracker.video.pitch_detection_video import ( + MODEL_PATH as pit_path, + ) + from forgesyte_yolo_tracker.video.player_detection_video import ( + MODEL_PATH as pd_path, + ) + from forgesyte_yolo_tracker.video.player_tracking_video import ( + MODEL_PATH as pt_path, + ) + from forgesyte_yolo_tracker.video.radar_video import PITCH_MODEL_PATH as r_pitch + from forgesyte_yolo_tracker.video.radar_video import ( + PLAYER_MODEL_PATH as r_player, + ) # All should contain "forgesyte_yolo_tracker/models" paths = [pd_path, pt_path, bd_path, pit_path, r_player, r_pitch] @@ -82,4 +91,6 @@ def test_all_video_modules_use_correct_models_path(self) -> None: ), f"Path should contain 'forgesyte_yolo_tracker': {path}" assert "models" in path, f"Path should contain 'models': {path}" # Ensure it's not using wrong src/models path - assert "/src/models" not in path, f"Path incorrectly points to src/models: {path}" + assert ( + "/src/models" not in path + ), f"Path incorrectly points to src/models: {path}" diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_ball.py b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_ball.py index b5b4459..c14f5aa 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_ball.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_ball.py @@ -152,7 +152,9 @@ def test_update_selects_closest_to_centroid(self) -> None: # Add first detection at (100, 100) det1 = sv.Detections( - xyxy=np.array([[75, 75, 125, 125]]), confidence=np.array([1.0]), class_id=np.array([0]) + xyxy=np.array([[75, 75, 125, 125]]), + confidence=np.array([1.0]), + class_id=np.array([0]), ) result1 = tracker.update(det1) assert len(result1) == 1 diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_soccer_pitch.py b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_soccer_pitch.py index 4acf962..c335369 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_soccer_pitch.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_soccer_pitch.py @@ -9,8 +9,11 @@ import supervision as sv from forgesyte_yolo_tracker.utils.soccer_pitch import ( - draw_paths_on_pitch, draw_pitch, draw_pitch_voronoi_diagram, - draw_points_on_pitch) + draw_paths_on_pitch, + draw_pitch, + draw_pitch_voronoi_diagram, + draw_points_on_pitch, +) class SoccerPitchConfig: @@ -47,21 +50,27 @@ def soccer_pitch_config() -> SoccerPitchConfig: class TestDrawPitchValidation: """Tests that validate actual drawing output for draw_pitch.""" - def test_pitch_returns_numpy_array(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_pitch_returns_numpy_array( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify draw_pitch returns numpy array.""" result = draw_pitch(soccer_pitch_config) assert isinstance(result, np.ndarray) assert result.dtype == np.uint8 assert len(result.shape) == 3 - def test_pitch_has_green_background(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_pitch_has_green_background( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify pitch background is green (34, 139, 34 BGR).""" result = draw_pitch(soccer_pitch_config) # Sample corner area (not on lines) corner_pixel = result[10, 10] assert corner_pixel[1] == 139 - def test_pitch_has_white_lines(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_pitch_has_white_lines( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify pitch lines are white (255, 255, 255).""" result = draw_pitch(soccer_pitch_config) # Check that white pixels exist on the image (from lines) @@ -79,7 +88,9 @@ def test_pitch_custom_colors(self, soccer_pitch_config: SoccerPitchConfig) -> No assert result.shape == default_result.shape assert isinstance(result, np.ndarray) - def test_pitch_centre_circle_exists(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_pitch_centre_circle_exists( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify centre circle is drawn (white pixels at center area).""" result = draw_pitch(soccer_pitch_config) center_y, center_x = result.shape[0] // 2, result.shape[1] // 2 @@ -94,13 +105,17 @@ def test_pitch_centre_circle_exists(self, soccer_pitch_config: SoccerPitchConfig class TestDrawPointsOnPitchValidation: """Tests for draw_points_on_pitch with real validation.""" - def test_points_returns_numpy_array(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_points_returns_numpy_array( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify draw_points_on_pitch returns numpy array.""" points = np.array([[50.0, 30.0]]) result = draw_points_on_pitch(soccer_pitch_config, points) assert isinstance(result, np.ndarray) - def test_points_scaled_correctly(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_points_scaled_correctly( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify points are drawn at scaled coordinates.""" pitch = np.ones((200, 200, 3), dtype=np.uint8) * 34 points = np.array([[100.0, 100.0]]) @@ -110,7 +125,9 @@ def test_points_scaled_correctly(self, soccer_pitch_config: SoccerPitchConfig) - point_pixel = result[60, 60] assert point_pixel[2] == 255 - def test_points_with_existing_pitch(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_points_with_existing_pitch( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify points can be drawn on existing pitch.""" existing_pitch = np.ones((200, 200, 3), dtype=np.uint8) * 50 points = np.array([[55.0, 53.0]]) # Point on boundary line @@ -138,7 +155,9 @@ def test_custom_point_radius(self, soccer_pitch_config: SoccerPitchConfig) -> No pitch = np.ones((200, 200, 3), dtype=np.uint8) * 34 points = np.array([[100.0, 100.0]]) - result = draw_points_on_pitch(soccer_pitch_config, points, pitch=pitch, radius=15) + result = draw_points_on_pitch( + soccer_pitch_config, points, pitch=pitch, radius=15 + ) center_area = result[45:76, 45:76] red_count = np.sum(center_area[:, :, 2] == 255) @@ -148,7 +167,9 @@ def test_custom_point_radius(self, soccer_pitch_config: SoccerPitchConfig) -> No class TestDrawPathsOnPitchValidation: """Tests for draw_paths_on_pitch with real validation.""" - def test_paths_returns_numpy_array(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_paths_returns_numpy_array( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify draw_paths_on_pitch returns numpy array.""" paths = [np.array([[50.0, 30.0], [60.0, 40.0]])] result = draw_paths_on_pitch(soccer_pitch_config, paths) @@ -168,13 +189,18 @@ def test_path_connects_points(self, soccer_pitch_config: SoccerPitchConfig) -> N def test_multiple_paths(self, soccer_pitch_config: SoccerPitchConfig) -> None: """Verify multiple paths are drawn.""" pitch = np.ones((200, 200, 3), dtype=np.uint8) * 34 - paths = [np.array([[50.0, 30.0], [60.0, 40.0]]), np.array([[70.0, 50.0], [80.0, 60.0]])] + paths = [ + np.array([[50.0, 30.0], [60.0, 40.0]]), + np.array([[70.0, 50.0], [80.0, 60.0]]), + ] result = draw_paths_on_pitch(soccer_pitch_config, paths, pitch=pitch) assert np.any(result[:, :, 0] == 255) - def test_path_with_existing_pitch(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_path_with_existing_pitch( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify paths can be drawn on existing pitch.""" existing_pitch = np.ones((200, 200, 3), dtype=np.uint8) * 50 path = np.array([[50.0, 30.0], [60.0, 40.0]]) @@ -187,7 +213,9 @@ def test_path_with_existing_pitch(self, soccer_pitch_config: SoccerPitchConfig) class TestDrawVoronoiDiagramValidation: """Tests for draw_pitch_voronoi_diagram with real validation.""" - def test_voronoi_returns_numpy_array(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_voronoi_returns_numpy_array( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify draw_pitch_voronoi_diagram returns numpy array.""" team_1 = np.array([[50.0, 30.0]]) team_2 = np.array([[60.0, 40.0]]) @@ -195,7 +223,9 @@ def test_voronoi_returns_numpy_array(self, soccer_pitch_config: SoccerPitchConfi result = draw_pitch_voronoi_diagram(soccer_pitch_config, team_1, team_2) assert isinstance(result, np.ndarray) - def test_voronoi_has_team_colors(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_voronoi_has_team_colors( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify Voronoi diagram has team colors (red and white).""" team_1 = np.array([[50.0, 30.0]]) team_2 = np.array([[60.0, 40.0]]) @@ -212,25 +242,33 @@ def test_voronoi_has_team_colors(self, soccer_pitch_config: SoccerPitchConfig) - has_white = np.any(result == 255) assert has_red or has_white - def test_voronoi_opacity_blending(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_voronoi_opacity_blending( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify opacity affects blending.""" team_1 = np.array([[50.0, 30.0]]) team_2 = np.array([[80.0, 50.0]]) - result_opaque = draw_pitch_voronoi_diagram(soccer_pitch_config, team_1, team_2, opacity=1.0) + result_opaque = draw_pitch_voronoi_diagram( + soccer_pitch_config, team_1, team_2, opacity=1.0 + ) result_transparent = draw_pitch_voronoi_diagram( soccer_pitch_config, team_1, team_2, opacity=0.5 ) assert not np.array_equal(result_opaque, result_transparent) - def test_voronoi_with_existing_pitch(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_voronoi_with_existing_pitch( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify Voronoi can be drawn on existing pitch.""" padding = 50 scale = 0.1 expected_height = int(soccer_pitch_config.width * scale) + 2 * padding expected_width = int(soccer_pitch_config.length * scale) + 2 * padding - existing_pitch = np.ones((expected_height, expected_width, 3), dtype=np.uint8) * 50 + existing_pitch = ( + np.ones((expected_height, expected_width, 3), dtype=np.uint8) * 50 + ) team_1 = np.array([[50.0, 30.0]]) team_2 = np.array([[60.0, 40.0]]) @@ -240,7 +278,9 @@ def test_voronoi_with_existing_pitch(self, soccer_pitch_config: SoccerPitchConfi assert result.shape == existing_pitch.shape - def test_voronoi_different_team_positions(self, soccer_pitch_config: SoccerPitchConfig) -> None: + def test_voronoi_different_team_positions( + self, soccer_pitch_config: SoccerPitchConfig + ) -> None: """Verify Voronoi changes with different team positions.""" team_1_a = np.array([[30.0, 30.0]]) team_2_a = np.array([[70.0, 30.0]]) diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_model.py b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_model.py index fe212c3..2caae49 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_model.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_model.py @@ -16,7 +16,8 @@ RUN_MODEL_TESTS = os.getenv("RUN_MODEL_TESTS", "0") == "1" pytestmark = pytest.mark.skipif( - not RUN_MODEL_TESTS, reason="Set RUN_MODEL_TESTS=1 to run (requires network for model loading)" + not RUN_MODEL_TESTS, + reason="Set RUN_MODEL_TESTS=1 to run (requires network for model loading)", ) @@ -49,7 +50,9 @@ def test_initialization_gpu(self) -> None: @patch("forgesyte_yolo_tracker.utils.team.tqdm") @patch("forgesyte_yolo_tracker.utils.team.torch") - def test_extract_features_basic(self, mock_torch: MagicMock, mock_tqdm: MagicMock) -> None: + def test_extract_features_basic( + self, mock_torch: MagicMock, mock_tqdm: MagicMock + ) -> None: """Test feature extraction from image crops.""" with ( patch("forgesyte_yolo_tracker.utils.team.SiglipVisionModel"), diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_prediction.py b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_prediction.py index 925694f..6e7c74f 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_prediction.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_team_prediction.py @@ -48,7 +48,9 @@ def test_predict_returns_numpy_array(self, mock_classifier: TeamClassifier) -> N assert isinstance(result, np.ndarray) - def test_predict_returns_binary_labels(self, mock_classifier: TeamClassifier) -> None: + def test_predict_returns_binary_labels( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() returns only 0 or 1 labels.""" mock_classifier.extract_features = MagicMock(return_value=np.random.rand(4, 512)) # type: ignore[method-assign] mock_classifier.reducer.transform = MagicMock(return_value=np.random.rand(4, 3)) # type: ignore[method-assign] @@ -60,7 +62,9 @@ def test_predict_returns_binary_labels(self, mock_classifier: TeamClassifier) -> assert len(result) == 4 assert all(label in [0, 1] for label in result) - def test_predict_calls_extract_features(self, mock_classifier: TeamClassifier) -> None: + def test_predict_calls_extract_features( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() calls extract_features().""" mock_classifier.extract_features = MagicMock(return_value=np.random.rand(2, 512)) # type: ignore[method-assign] mock_classifier.reducer.transform = MagicMock(return_value=np.random.rand(2, 3)) # type: ignore[method-assign] @@ -70,7 +74,9 @@ def test_predict_calls_extract_features(self, mock_classifier: TeamClassifier) - mock_classifier.extract_features.assert_called_once_with(crops) - def test_predict_calls_reducer_transform(self, mock_classifier: TeamClassifier) -> None: + def test_predict_calls_reducer_transform( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() calls reducer.transform().""" mock_classifier.extract_features = MagicMock(return_value=np.random.rand(2, 512)) # type: ignore[method-assign] mock_classifier.reducer.transform = MagicMock(return_value=np.random.rand(2, 3)) # type: ignore[method-assign] @@ -80,7 +86,9 @@ def test_predict_calls_reducer_transform(self, mock_classifier: TeamClassifier) mock_classifier.reducer.transform.assert_called_once() - def test_predict_calls_cluster_predict(self, mock_classifier: TeamClassifier) -> None: + def test_predict_calls_cluster_predict( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() calls cluster_model.predict().""" mock_classifier.extract_features = MagicMock(return_value=np.random.rand(2, 512)) # type: ignore[method-assign] mock_classifier.reducer.transform = MagicMock(return_value=np.random.rand(2, 3)) # type: ignore[method-assign] @@ -135,27 +143,34 @@ def test_predict_large_batch(self, mock_classifier: TeamClassifier) -> None: assert len(result) == 10 mock_classifier.extract_features.assert_called_once() - def test_predict_empty_crops_returns_empty_array(self, mock_classifier: TeamClassifier) -> None: + def test_predict_empty_crops_returns_empty_array( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() with empty crops returns empty numpy array.""" result = mock_classifier.predict([]) assert isinstance(result, np.ndarray) assert len(result) == 0 - def test_predict_result_shape_matches_input(self, mock_classifier: TeamClassifier) -> None: + def test_predict_result_shape_matches_input( + self, mock_classifier: TeamClassifier + ) -> None: """Verify predict() returns array with same length as input crops.""" num_crops = 7 mock_classifier.extract_features = MagicMock(return_value=np.random.rand(num_crops, 512)) # type: ignore[method-assign] mock_classifier.reducer.transform = MagicMock(return_value=np.random.rand(num_crops, 3)) # type: ignore[method-assign] - mock_classifier.cluster_model.predict.return_value = np.random.randint(0, 2, size=num_crops) + mock_classifier.cluster_model.predict.return_value = np.random.randint( + 0, 2, size=num_crops + ) crops = [np.zeros((224, 224, 3), dtype=np.uint8) for _ in range(num_crops)] result = mock_classifier.predict(crops) assert len(result) == num_crops - - def test_cluster_model_n_clusters_is_2(self, mock_classifier: TeamClassifier) -> None: + def test_cluster_model_n_clusters_is_2( + self, mock_classifier: TeamClassifier + ) -> None: """Verify cluster_model is configured with 2 clusters.""" mock_classifier.cluster_model.n_clusters = 2 assert mock_classifier.cluster_model.n_clusters == 2 diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_view.py b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_view.py index c28020f..697c4da 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_view.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/utils/test_view.py @@ -11,10 +11,13 @@ class TestViewTransformer: def test_initialization_valid(self) -> None: """Test ViewTransformer initialization with valid inputs.""" - source = np.array([[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=np.float32) + source = np.array( + [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]], dtype=np.float32 + ) target = np.array( - [[10.0, 10.0], [110.0, 10.0], [10.0, 110.0], [110.0, 110.0]], dtype=np.float32 + [[10.0, 10.0], [110.0, 10.0], [10.0, 110.0], [110.0, 110.0]], + dtype=np.float32, ) transformer = ViewTransformer(source, target) diff --git a/plugins/forgesyte-yolo-tracker/tests_heavy/video/test_video_model_paths.py b/plugins/forgesyte-yolo-tracker/tests_heavy/video/test_video_model_paths.py index cef7d50..06aeb2d 100644 --- a/plugins/forgesyte-yolo-tracker/tests_heavy/video/test_video_model_paths.py +++ b/plugins/forgesyte-yolo-tracker/tests_heavy/video/test_video_model_paths.py @@ -12,8 +12,7 @@ class TestVideoModelPaths: def test_player_detection_video_model_path_is_resolved(self) -> None: """Verify player detection video uses config-based model path.""" - from forgesyte_yolo_tracker.video.player_detection_video import \ - MODEL_PATH + from forgesyte_yolo_tracker.video.player_detection_video import MODEL_PATH assert isinstance(MODEL_PATH, str) assert MODEL_PATH.endswith(".pt") @@ -22,8 +21,7 @@ def test_player_detection_video_model_path_is_resolved(self) -> None: def test_player_tracking_video_model_path_is_resolved(self) -> None: """Verify player tracking video uses config-based model path.""" - from forgesyte_yolo_tracker.video.player_tracking_video import \ - MODEL_PATH + from forgesyte_yolo_tracker.video.player_tracking_video import MODEL_PATH assert isinstance(MODEL_PATH, str) assert MODEL_PATH.endswith(".pt") @@ -32,8 +30,7 @@ def test_player_tracking_video_model_path_is_resolved(self) -> None: def test_ball_detection_video_model_path_is_resolved(self) -> None: """Verify ball detection video uses config-based model path.""" - from forgesyte_yolo_tracker.video.ball_detection_video import \ - MODEL_PATH + from forgesyte_yolo_tracker.video.ball_detection_video import MODEL_PATH assert isinstance(MODEL_PATH, str) assert MODEL_PATH.endswith(".pt") @@ -42,8 +39,7 @@ def test_ball_detection_video_model_path_is_resolved(self) -> None: def test_pitch_detection_video_model_path_is_resolved(self) -> None: """Verify pitch detection video uses config-based model path.""" - from forgesyte_yolo_tracker.video.pitch_detection_video import \ - MODEL_PATH + from forgesyte_yolo_tracker.video.pitch_detection_video import MODEL_PATH assert isinstance(MODEL_PATH, str) assert MODEL_PATH.endswith(".pt") @@ -70,18 +66,22 @@ def test_radar_video_pitch_model_path_is_resolved(self) -> None: def test_all_video_model_paths_use_absolute_path(self) -> None: """Verify all video model paths are absolute.""" - from forgesyte_yolo_tracker.video.ball_detection_video import \ - MODEL_PATH as bd_path - from forgesyte_yolo_tracker.video.pitch_detection_video import \ - MODEL_PATH as pit_path - from forgesyte_yolo_tracker.video.player_detection_video import \ - MODEL_PATH as pd_path - from forgesyte_yolo_tracker.video.player_tracking_video import \ - MODEL_PATH as pt_path - from forgesyte_yolo_tracker.video.radar_video import \ - PITCH_MODEL_PATH as r_pitch - from forgesyte_yolo_tracker.video.radar_video import \ - PLAYER_MODEL_PATH as r_player + from forgesyte_yolo_tracker.video.ball_detection_video import ( + MODEL_PATH as bd_path, + ) + from forgesyte_yolo_tracker.video.pitch_detection_video import ( + MODEL_PATH as pit_path, + ) + from forgesyte_yolo_tracker.video.player_detection_video import ( + MODEL_PATH as pd_path, + ) + from forgesyte_yolo_tracker.video.player_tracking_video import ( + MODEL_PATH as pt_path, + ) + from forgesyte_yolo_tracker.video.radar_video import PITCH_MODEL_PATH as r_pitch + from forgesyte_yolo_tracker.video.radar_video import ( + PLAYER_MODEL_PATH as r_player, + ) for path in [pd_path, pt_path, bd_path, pit_path, r_player, r_pitch]: p = Path(path)