Spaces:
Running
Running
| """ | |
| Load the stroke model from disk using checkpoint metadata + model_registry.json. | |
| Checkpoints from training may be a raw state_dict (CNN-LSTM) or a dict envelope with | |
| ``model``, ``architecture``, ``task_classes``, and constructor hints. Registry entries | |
| can override or supply ``architecture`` / ``inference`` when older checkpoints omit fields. | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import os | |
| from typing import Any, Dict, Tuple | |
| import torch | |
| import torch.nn as nn | |
| from core.model import CNN_LSTM_Model | |
| ARCH_CNN_LSTM = "cnn_lstm" | |
| ARCH_TIMESFORMER = "timesformer" | |
| ARCH_VIDEOMAE_POSE = "videomae_pose" | |
| ARCH_VIDEOMAE_TIMESFORMER = "videomae_timesformer" | |
| ARCH_STAE = "staeformer" | |
| _SCRIPT_TO_ARCH = { | |
| "train_full.py": ARCH_CNN_LSTM, | |
| "train_timesformer.py": ARCH_TIMESFORMER, | |
| "train_videomae.py": ARCH_VIDEOMAE_POSE, | |
| "train_videomae_timesformer.py": ARCH_VIDEOMAE_TIMESFORMER, | |
| "train_staeformer.py": ARCH_STAE, | |
| } | |
| def split_checkpoint(raw: Any) -> Tuple[Dict[str, Any], Dict[str, torch.Tensor]]: | |
| if isinstance(raw, dict) and "model" in raw and isinstance(raw["model"], dict): | |
| meta = {k: v for k, v in raw.items() if k != "model"} | |
| return meta, raw["model"] | |
| if isinstance(raw, dict): | |
| return {}, raw | |
| raise TypeError(f"Unexpected checkpoint type: {type(raw)}") | |
| def _merge_inference(registry_meta: Dict[str, Any]) -> Dict[str, Any]: | |
| out = dict(registry_meta.get("inference") or {}) | |
| for k in ("architecture", "hf_model_id", "embed_dim", "depth", "num_heads"): | |
| if k in registry_meta and k not in out: | |
| out[k] = registry_meta[k] | |
| return out | |
| def resolve_architecture( | |
| ckpt_meta: Dict[str, Any], | |
| registry_meta: Dict[str, Any], | |
| filename: str, | |
| ) -> str: | |
| for src in (ckpt_meta, _merge_inference(registry_meta), registry_meta): | |
| a = src.get("architecture") | |
| if a: | |
| return str(a).lower().replace("-", "_") | |
| script = registry_meta.get("script") | |
| if script in _SCRIPT_TO_ARCH: | |
| return _SCRIPT_TO_ARCH[script] | |
| fn = filename.lower() | |
| if "videomae" in fn and "timesformer" in fn: | |
| return ARCH_VIDEOMAE_TIMESFORMER | |
| if "videomae" in fn: | |
| return ARCH_VIDEOMAE_POSE | |
| if "timesformer" in fn: | |
| return ARCH_TIMESFORMER | |
| if "staeformer" in fn and "timesformer" not in fn: | |
| return ARCH_STAE | |
| return ARCH_CNN_LSTM | |
| def build_model( | |
| arch: str, | |
| task_classes: Dict[str, int], | |
| ckpt_meta: Dict[str, Any], | |
| registry_meta: Dict[str, Any], | |
| ) -> nn.Module: | |
| inf = _merge_inference(registry_meta) | |
| def _i(key: str, default: Any = None) -> Any: | |
| if key in ckpt_meta: | |
| return ckpt_meta[key] | |
| if key in inf: | |
| return inf[key] | |
| return default | |
| if arch == ARCH_STAE: | |
| raise RuntimeError( | |
| "STAEformer checkpoints are not supported by the /analyze API " | |
| "(they need per-frame CNN features). Switch active_model to a CNN-LSTM, " | |
| "TimeSformer, or VideoMAE checkpoint." | |
| ) | |
| if arch == ARCH_CNN_LSTM: | |
| hidden = int(_i("hidden_size", registry_meta.get("hidden_size", 128))) | |
| use_pose = bool(_i("use_pose", False)) | |
| return CNN_LSTM_Model(task_classes=task_classes, hidden_size=hidden, pretrained=False, use_pose=use_pose) | |
| if arch == ARCH_TIMESFORMER: | |
| from core.timesformer import TimeSformerPoseModel | |
| return TimeSformerPoseModel( | |
| task_classes=task_classes, | |
| img_size=224, | |
| patch_size=16, | |
| num_frames=int(_i("num_frames", 16)), | |
| embed_dim=int(_i("embed_dim", 128)), | |
| num_heads=int(_i("num_heads", 4)), | |
| depth=int(_i("depth", 4)), | |
| backbone=str(_i("backbone", "vit")), | |
| vit_model_name=str(_i("vit_model_name", "vit_small_patch16_224")), | |
| vit_unfreeze_last_n=int(_i("vit_unfreeze_last_n", 0)), | |
| ) | |
| if arch == ARCH_VIDEOMAE_POSE: | |
| from core.videomae_pose import VideoMAEPoseModel | |
| return VideoMAEPoseModel( | |
| task_classes=task_classes, | |
| hf_model_id=str(_i("hf_model_id", "MCG-NJU/videomae-base")), | |
| num_frames=int(_i("num_frames", 16)), | |
| freeze_backbone=bool(_i("freeze_videomae", _i("freeze_backbone", True))), | |
| unfreeze_last_n=int(_i("videomae_unfreeze_last_n", _i("unfreeze_last_n", 0))), | |
| ) | |
| if arch == ARCH_VIDEOMAE_TIMESFORMER: | |
| from core.videomae_timesformer import VideoMAETimeSformerPoseModel | |
| return VideoMAETimeSformerPoseModel( | |
| task_classes=task_classes, | |
| hf_model_id=str(_i("hf_model_id", "MCG-NJU/videomae-base")), | |
| num_frames=int(_i("num_frames", 16)), | |
| embed_dim=int(_i("embed_dim", 128)), | |
| num_heads=int(_i("num_heads", 4)), | |
| depth=int(_i("depth", 4)), | |
| freeze_videomae=bool(_i("freeze_videomae", True)), | |
| videomae_unfreeze_last_n=int(_i("videomae_unfreeze_last_n", 0)), | |
| ) | |
| raise ValueError(f"Unknown architecture {arch!r}") | |
| def load_stroke_model( | |
| model_path: str, | |
| task_classes: Dict[str, int], | |
| registry: Dict[str, Any], | |
| device: str, | |
| ) -> Tuple[nn.Module, str]: | |
| raw = torch.load(model_path, map_location=device, weights_only=False) | |
| ckpt_meta, state_dict = split_checkpoint(raw) | |
| name = os.path.basename(model_path) | |
| active = registry.get("active_model") | |
| reg_models = registry.get("models") or {} | |
| registry_meta = reg_models.get(name, {}) if name in reg_models else {} | |
| arch = resolve_architecture(ckpt_meta, registry_meta, name) | |
| if ckpt_meta.get("task_classes"): | |
| tc = ckpt_meta["task_classes"] | |
| if isinstance(tc, dict): | |
| task_classes = {k: int(v) for k, v in tc.items()} | |
| model = build_model(arch, task_classes, ckpt_meta, registry_meta) | |
| missing, unexpected = model.load_state_dict(state_dict, strict=False) | |
| if missing: | |
| print(f"WARNING: Missing keys (partial load / random init for those layers): {len(missing)} keys") | |
| if unexpected: | |
| print(f"WARNING: Unexpected keys ignored: {len(unexpected)} keys") | |
| return model, arch | |
| def load_registry(models_dir: str) -> Dict[str, Any]: | |
| path = os.path.join(models_dir, "model_registry.json") | |
| if not os.path.isfile(path): | |
| return {} | |
| with open(path, encoding="utf-8") as f: | |
| return json.load(f) | |
| def resolve_model_path(models_dir: str, registry: Dict[str, Any]) -> str | None: | |
| """Prefer active_model; else highest-accuracy non-STAEformer file (dev / multi-weight trees).""" | |
| active = registry.get("active_model") | |
| if active: | |
| p = os.path.join(models_dir, active) | |
| if os.path.isfile(p): | |
| return p | |
| best_name, best_acc = None, -1.0 | |
| for name, meta in (registry.get("models") or {}).items(): | |
| nl = name.lower() | |
| if "staeformer" in nl and "timesformer" not in nl: | |
| continue | |
| path = os.path.join(models_dir, name) | |
| if not os.path.isfile(path): | |
| continue | |
| acc = float(meta.get("accuracy", 0.0)) | |
| if acc > best_acc: | |
| best_acc = acc | |
| best_name = name | |
| if best_name: | |
| return os.path.join(models_dir, best_name) | |
| fallback = os.path.join(models_dir, "badminton_model.pth") | |
| return fallback if os.path.isfile(fallback) else None | |