Download inference.py from grasant/canrun-fps-predictor: direct link, hf CLI and curl.
- Browser
- Download file 16.1 kB
-
https://huggingface.co/grasant/canrun-fps-predictor/resolve/main/inference.py
- Command line
-
hf download hf://grasant/canrun-fps-predictor/inference.py
-
curl -L -o inference.py https://huggingface.co/grasant/canrun-fps-predictor/resolve/main/inference.py
16.1 kB
| """ | |
| Standalone, zero-dependency pure-Python inference for CanRun FPS Model v1.0. | |
| Executes the 21-feature physical engineering pipeline and the Two-Stage | |
| Standardized Log-Space Ridge Prior + Monotonic Huber GBDT Residual ensemble | |
| using only Python's standard library (`json`, `math`, `re`, `pathlib`). | |
| """ | |
| from __future__ import annotations | |
| import json | |
| import math | |
| import re | |
| import urllib.request | |
| from pathlib import Path | |
| from typing import Any | |
| MODEL_VERSION = "1.0" | |
| MODEL_NAME = "canrun-fps-predictor" | |
| FEATURE_NAMES: list[str] = [ | |
| "log_gpu_passmark", | |
| "log_cpu_passmark", | |
| "gpu_vram_gb", | |
| "gpu_arch_code", | |
| "cpu_to_gpu_log_ratio", | |
| "gpu_tier_score", | |
| "log_pixel_ratio", | |
| "vram_per_mpixel", | |
| "is_4k", | |
| "log_rec_gpu_passmark", | |
| "log_min_gpu_passmark", | |
| "gpu_headroom_rec", | |
| "gpu_headroom_min", | |
| "vram_headroom_gb", | |
| "spec_demand_index", | |
| "headroom_x_resolution", | |
| "cpu_bottleneck_1080p", | |
| "vram_cliff_penalty", | |
| "is_capped_60", | |
| "genre_demand", | |
| "cpu_headroom_rec", | |
| ] | |
| MONOTONIC_CONSTRAINTS: list[int] = [ | |
| 1, # log_gpu_passmark | |
| 1, # log_cpu_passmark | |
| 1, # gpu_vram_gb | |
| 1, # gpu_arch_code | |
| 1, # cpu_to_gpu_log_ratio | |
| 1, # gpu_tier_score | |
| -1, # log_pixel_ratio | |
| 1, # vram_per_mpixel | |
| -1, # is_4k | |
| 0, # log_rec_gpu_passmark | |
| 0, # log_min_gpu_passmark | |
| 0, # gpu_headroom_rec | |
| 0, # gpu_headroom_min | |
| 0, # vram_headroom_gb | |
| 0, # spec_demand_index | |
| 0, # headroom_x_resolution | |
| 1, # cpu_bottleneck_1080p | |
| 0, # vram_cliff_penalty | |
| -1, # is_capped_60 | |
| -1, # genre_demand | |
| 1, # cpu_headroom_rec | |
| ] | |
| RESOLUTION_PIXELS: dict[str, int] = { | |
| "720p": 1280 * 720, | |
| "1080p": 1920 * 1080, | |
| "1440p": 2560 * 1440, | |
| "4K": 3840 * 2160, | |
| } | |
| BASE_PIXELS_1080P = float(1920 * 1080) | |
| CAPPED_60_TITLE_MARKERS: tuple[str, ...] = ( | |
| "elden ring", | |
| "sekiro", | |
| "dark souls", | |
| "bloodborne", | |
| "street fighter", | |
| "tekken", | |
| "guilty gear", | |
| "mortal kombat", | |
| ) | |
| def infer_gpu_arch_code(gpu_name: str, gen_hint: str = "") -> float: | |
| """Map GPU model or generation string to ordinal architecture code [1.0 .. 5.0].""" | |
| text = f"{gpu_name} {gen_hint}".lower() | |
| if "rtx_50" in text or re.search(r"\brtx\s*50\d{2}\b", text) or "blackwell" in text: | |
| return 5.0 | |
| if ( | |
| "rtx_40" in text | |
| or re.search(r"\brtx\s*40\d{2}\b", text) | |
| or "rx_7000" in text | |
| or re.search(r"\brx\s*7\d{3}\b", text) | |
| or "ada" in text | |
| ): | |
| return 4.0 | |
| if ( | |
| "rtx_30" in text | |
| or re.search(r"\brtx\s*30\d{2}\b", text) | |
| or "rx_6000" in text | |
| or re.search(r"\brx\s*6\d{3}\b", text) | |
| or "ampere" in text | |
| ): | |
| return 3.0 | |
| if ( | |
| "rtx_20" in text | |
| or "gtx_16" in text | |
| or re.search(r"\b(?:rtx\s*20|gtx\s*16)\d{2}\b", text) | |
| or "turing" in text | |
| ): | |
| return 2.0 | |
| return 1.0 | |
| def normalize_resolution_label(resolution: str | None) -> str: | |
| """Normalize resolution strings to one of 720p, 1080p, 1440p, 4K.""" | |
| cleaned = re.sub(r"[^a-z0-9]+", "", (resolution or "1080p").lower()) | |
| if cleaned in {"4k", "2160p", "3840x2160", "uhd"}: | |
| return "4K" | |
| if cleaned in {"1440p", "2560x1440", "qhd", "2k"}: | |
| return "1440p" | |
| if cleaned in {"720p", "1280x720", "hd"}: | |
| return "720p" | |
| return "1080p" | |
| def infer_capped_60(game_name: str = "", genre: str = "") -> float: | |
| """Detect a 60 FPS engine lock from title/genre text — never from a game ID.""" | |
| text = f"{game_name} {genre}".lower() | |
| if any(marker in text for marker in CAPPED_60_TITLE_MARKERS): | |
| return 1.0 | |
| if "fighting" in text and "engine lock" in text: | |
| return 1.0 | |
| return 0.0 | |
| def infer_genre_demand_offset(genre: str = "") -> float: | |
| """Map genre string from Steam Store metadata to physical rendering load offset [-0.35 .. +0.25].""" | |
| g = (genre or "").lower() | |
| if any(k in g for k in ("esports", "competitive", "moba", "sports", "counter-strike", "valorant")): | |
| return -0.35 | |
| if any(k in g for k in ("indie", "platformer", "2d", "roguelike", "metroidvania", "puzzle", "card")): | |
| return -0.25 | |
| if any(k in g for k in ("aaa", "horror", "simulation", "flight", "cyberpunk", "open world")): | |
| return 0.25 | |
| if "rpg" in g: | |
| return 0.15 | |
| return 0.0 | |
| def extract_feature_vector( | |
| *, | |
| gpu_passmark: float, | |
| cpu_passmark: float = 28000.0, | |
| gpu_vram_gb: float = 8.0, | |
| gpu_name: str = "", | |
| gpu_gen: str = "", | |
| resolution: str = "1080p", | |
| rec_gpu_passmark: float = 0.0, | |
| min_gpu_passmark: float = 0.0, | |
| rec_cpu_passmark: float = 13000.0, | |
| rec_vram_gb: float = 6.0, | |
| rec_ram_gb: float = 16.0, | |
| release_year: int = 2023, | |
| genre: str = "", | |
| is_capped_60: float = 0.0, | |
| game_name: str = "", | |
| ) -> list[float]: | |
| """Compute the 21 leak-free domain features in deterministic FEATURE_NAMES order.""" | |
| safe_gpu_pm = max(400.0, float(gpu_passmark or 400.0)) | |
| safe_cpu_pm = max(1500.0, float(cpu_passmark or 28000.0)) | |
| safe_vram = max(1.0, float(gpu_vram_gb or 8.0)) | |
| if rec_gpu_passmark <= 0 and min_gpu_passmark > 0: | |
| safe_min_gpu_pm = max(400.0, float(min_gpu_passmark)) | |
| safe_rec_gpu_pm = safe_min_gpu_pm * 1.65 | |
| elif rec_gpu_passmark > 0 and min_gpu_passmark <= 0: | |
| safe_rec_gpu_pm = max(400.0, float(rec_gpu_passmark)) | |
| safe_min_gpu_pm = max(400.0, safe_rec_gpu_pm / 1.65) | |
| elif rec_gpu_passmark <= 0 and min_gpu_passmark <= 0: | |
| safe_rec_gpu_pm = 6385.0 | |
| safe_min_gpu_pm = 3800.0 | |
| else: | |
| safe_rec_gpu_pm = max(400.0, float(rec_gpu_passmark)) | |
| safe_min_gpu_pm = max(400.0, min(safe_rec_gpu_pm, float(min_gpu_passmark))) | |
| safe_rec_cpu_pm = max(1500.0, float(rec_cpu_passmark or 13000.0)) | |
| safe_rec_vram = max(1.0, float(rec_vram_gb or 6.0)) | |
| safe_rec_ram = max(4.0, float(rec_ram_gb or 16.0)) | |
| safe_year = max(2012, min(2027, int(release_year or 2023))) | |
| genre_offset = infer_genre_demand_offset(genre) | |
| res_key = normalize_resolution_label(resolution) | |
| pixels = float(RESOLUTION_PIXELS.get(res_key, RESOLUTION_PIXELS["1080p"])) | |
| mpixels = pixels / 1_000_000.0 | |
| log_gpu_passmark = math.log(safe_gpu_pm) | |
| log_cpu_passmark = math.log(safe_cpu_pm) | |
| raw_arch = infer_gpu_arch_code(gpu_name, gpu_gen) | |
| gpu_arch_code = 1.0 + 3.8 * (safe_gpu_pm / 40000.0) + 0.01 * raw_arch | |
| effective_vram = 4.0 + 20.0 * (safe_gpu_pm / 40000.0) + 0.008 * min(24.0, safe_vram) | |
| cpu_to_gpu_log_ratio = 0.75 * log_gpu_passmark + 0.25 * log_cpu_passmark | |
| gpu_tier_score = safe_gpu_pm / 40000.0 | |
| log_pixel_ratio = math.log(pixels / BASE_PIXELS_1080P) | |
| vram_per_mpixel = effective_vram / mpixels | |
| is_4k = 1.0 if res_key == "4K" else 0.0 | |
| log_rec_gpu_passmark = math.log(safe_rec_gpu_pm) | |
| log_min_gpu_passmark = math.log(safe_min_gpu_pm) | |
| gpu_headroom_rec = log_gpu_passmark - log_rec_gpu_passmark | |
| gpu_headroom_min = log_gpu_passmark - log_min_gpu_passmark | |
| vram_headroom_gb = effective_vram - safe_rec_vram | |
| spec_demand_index = ( | |
| 0.50 * ((log_rec_gpu_passmark - math.log(4000.0)) / (math.log(32000.0) - math.log(4000.0))) | |
| + 0.18 * ((math.log(safe_rec_cpu_pm) - math.log(4000.0)) / (math.log(25000.0) - math.log(4000.0))) | |
| + 0.12 * (min(32.0, safe_rec_ram) / 32.0) | |
| + 0.10 * ((safe_year - 2015.0) / 11.0) | |
| + genre_offset | |
| ) | |
| headroom_x_resolution = gpu_headroom_rec * (1.0 + 0.25 * max(0.0, log_pixel_ratio)) | |
| cpu_bottleneck_1080p = log_cpu_passmark / (1.0 + 0.35 * max(0.0, log_pixel_ratio)) | |
| vram_cliff_penalty = max(0.0, safe_rec_vram * (1.0 + 0.35 * is_4k) - effective_vram) | |
| capped_60 = 1.0 if float(is_capped_60 or 0.0) >= 0.5 else infer_capped_60(game_name, genre) | |
| cpu_headroom_rec = log_cpu_passmark - math.log(safe_rec_cpu_pm) | |
| return [ | |
| log_gpu_passmark, | |
| log_cpu_passmark, | |
| effective_vram, | |
| gpu_arch_code, | |
| cpu_to_gpu_log_ratio, | |
| gpu_tier_score, | |
| log_pixel_ratio, | |
| vram_per_mpixel, | |
| is_4k, | |
| log_rec_gpu_passmark, | |
| log_min_gpu_passmark, | |
| gpu_headroom_rec, | |
| gpu_headroom_min, | |
| vram_headroom_gb, | |
| spec_demand_index, | |
| headroom_x_resolution, | |
| cpu_bottleneck_1080p, | |
| vram_cliff_penalty, | |
| capped_60, | |
| genre_offset, | |
| cpu_headroom_rec, | |
| ] | |
| class CanRunFPSModel: | |
| """ | |
| Zero-dependency Pure-Python inference engine for CanRun FPS Model v1.0. | |
| """ | |
| def __init__(self, weights: dict[str, Any]) -> None: | |
| self.weights = weights | |
| self.version = str(weights.get("version", MODEL_VERSION)) | |
| self.feature_names: list[str] = list(weights["feature_names"]) | |
| self.scaler_mean: list[float] = [float(v) for v in weights["scaler_mean"]] | |
| self.scaler_scale: list[float] = [float(v) for v in weights["scaler_scale"]] | |
| self.ridge_weights: list[float] = [float(v) for v in weights["ridge_weights"]] | |
| self.ridge_intercept: float = float(weights["ridge_intercept"]) | |
| self.gbdt_baseline: float = float(weights["gbdt_baseline"]) | |
| self.gbdt_trees: list[list[dict[str, Any]]] = weights["gbdt_trees"] | |
| self.conformal_quantiles: dict[str, dict[str, float]] = weights.get("conformal_quantiles", {}) | |
| def from_pretrained( | |
| cls, | |
| model_id_or_path: str | Path = "grasant/canrun-fps-predictor", | |
| *, | |
| filename: str = "model.json", | |
| ) -> CanRunFPSModel: | |
| """ | |
| Load `model.json` from a local directory/file path or download from Hugging Face Hub. | |
| """ | |
| candidate = Path(model_id_or_path) | |
| if candidate.is_file(): | |
| data = json.loads(candidate.read_text(encoding="utf-8")) | |
| return cls(data) | |
| if candidate.is_dir() and (candidate / filename).is_file(): | |
| data = json.loads((candidate / filename).read_text(encoding="utf-8")) | |
| return cls(data) | |
| local_default = Path(__file__).resolve().parent / filename | |
| if str(model_id_or_path) in {".", "grasant/canrun-fps-predictor", "grasant/canrun"} and local_default.is_file(): | |
| data = json.loads(local_default.read_text(encoding="utf-8")) | |
| return cls(data) | |
| from huggingface_hub import hf_hub_download | |
| downloaded = Path(hf_hub_download(repo_id=str(model_id_or_path), filename=filename)) | |
| data = json.loads(downloaded.read_text(encoding="utf-8")) | |
| return cls(data) | |
| def predict_from_features(self, x_vec: list[float]) -> float: | |
| """Execute Stage-0 Log-Ridge Prior + Stage-1 Monotonic GBDT in pure Python.""" | |
| log_prior = self.ridge_intercept | |
| for x_val, mu, sc, w in zip(x_vec, self.scaler_mean, self.scaler_scale, self.ridge_weights): | |
| z = (x_val - mu) / sc if sc != 0.0 else 0.0 | |
| log_prior += z * w | |
| residual = self.gbdt_baseline | |
| for tree_nodes in self.gbdt_trees: | |
| idx = 0 | |
| while not tree_nodes[idx]["leaf"]: | |
| node = tree_nodes[idx] | |
| f_val = x_vec[node["f"]] | |
| if f_val <= node["t"]: | |
| idx = node["l"] | |
| else: | |
| idx = node["r"] | |
| residual += float(tree_nodes[idx]["v"]) | |
| log_fps = max(math.log(5.0), min(math.log(500.0), log_prior + residual)) | |
| return math.exp(log_fps) | |
| def predict_fps( | |
| self, | |
| *, | |
| gpu_passmark: float, | |
| cpu_passmark: float = 28000.0, | |
| gpu_vram_gb: float = 8.0, | |
| gpu_name: str = "", | |
| gpu_gen: str = "", | |
| resolution: str = "1080p", | |
| rec_gpu_passmark: float = 0.0, | |
| min_gpu_passmark: float = 0.0, | |
| rec_cpu_passmark: float = 13000.0, | |
| rec_vram_gb: float = 6.0, | |
| rec_ram_gb: float = 16.0, | |
| release_year: int = 2023, | |
| genre: str = "", | |
| is_capped_60: float = 0.0, | |
| game_name: str = "", | |
| ) -> float: | |
| """Predict rasterized FPS for the given hardware and Steam game specification.""" | |
| x_vec = extract_feature_vector( | |
| gpu_passmark=gpu_passmark, | |
| cpu_passmark=cpu_passmark, | |
| gpu_vram_gb=gpu_vram_gb, | |
| gpu_name=gpu_name, | |
| gpu_gen=gpu_gen, | |
| resolution=resolution, | |
| rec_gpu_passmark=rec_gpu_passmark, | |
| min_gpu_passmark=min_gpu_passmark, | |
| rec_cpu_passmark=rec_cpu_passmark, | |
| rec_vram_gb=rec_vram_gb, | |
| rec_ram_gb=rec_ram_gb, | |
| release_year=release_year, | |
| genre=genre, | |
| is_capped_60=is_capped_60, | |
| game_name=game_name, | |
| ) | |
| fps = self.predict_from_features(x_vec) | |
| if x_vec[18] >= 0.5: | |
| fps = min(60.0, fps) | |
| return round(fps, 2) | |
| def predict_with_interval( | |
| self, | |
| *, | |
| regime: str = "unseen_game_spec", | |
| **kwargs: Any, | |
| ) -> dict[str, float | str]: | |
| """Return point FPS prediction and 80% split-conformal log interval.""" | |
| res_key = normalize_resolution_label(str(kwargs.get("resolution", "1080p"))) | |
| fps = self.predict_fps(**kwargs) | |
| q_map = self.conformal_quantiles.get(regime, {}) | |
| q = float(q_map.get(res_key, 0.0535)) | |
| fps_low = round(fps * math.exp(-q), 1) | |
| fps_high = round(fps * math.exp(q), 1) | |
| if float(kwargs.get("is_capped_60", 0.0) or 0.0) >= 0.5 or infer_capped_60( | |
| str(kwargs.get("game_name", "")), str(kwargs.get("genre", "")) | |
| ): | |
| fps_low = min(60.0, fps_low) | |
| fps_high = min(60.0, fps_high) | |
| return { | |
| "expected_fps": fps, | |
| "conformal_80_low": fps_low, | |
| "conformal_80_high": fps_high, | |
| "resolution": res_key, | |
| "model_version": self.version, | |
| } | |
| def fetch_steam_store_metadata(app_id: int | str, timeout: float = 10.0) -> dict[str, Any]: | |
| """ | |
| Fetch public game requirements and metadata directly from Valve's unauthenticated | |
| Steam Store API (`store.steampowered.com/api/appdetails`) at runtime without | |
| redistributing Steam's store database. | |
| """ | |
| clean_id = str(app_id).strip() | |
| if not clean_id.isdigit(): | |
| raise ValueError(f"Steam app_id must be numeric, got {app_id!r}") | |
| url = f"https://store.steampowered.com/api/appdetails?appids={clean_id}&l=english" | |
| req = urllib.request.Request(url, headers={"User-Agent": "CanRunFPSPredictor/1.0"}) | |
| with urllib.request.urlopen(req, timeout=timeout) as resp: | |
| payload = json.loads(resp.read().decode("utf-8")) | |
| entry = payload.get(clean_id, {}) | |
| if not entry.get("success") or not isinstance(entry.get("data"), dict): | |
| raise RuntimeError(f"Steam Store API returned no data for app_id={clean_id}") | |
| data = entry["data"] | |
| pc_req = data.get("pc_requirements") or {} | |
| genres = ", ".join(g.get("description", "") for g in (data.get("genres") or []) if isinstance(g, dict)) | |
| release_str = str((data.get("release_date") or {}).get("date", "")) | |
| year_match = re.search(r"\b(19\d{2}|20\d{2})\b", release_str) | |
| release_year = int(year_match.group(1)) if year_match else 2023 | |
| return { | |
| "steam_app_id": int(clean_id), | |
| "game_name": str(data.get("name", "")), | |
| "release_year": release_year, | |
| "genre": genres, | |
| "minimum_html": str(pc_req.get("minimum", "")) if isinstance(pc_req, dict) else "", | |
| "recommended_html": str(pc_req.get("recommended", "")) if isinstance(pc_req, dict) else "", | |
| } | |