ai-model-explorer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ai_model_explorer/__init__.py +13 -0
- ai_model_explorer/__main__.py +5 -0
- ai_model_explorer/_engine.py +100 -0
- ai_model_explorer/_hub.py +50 -0
- ai_model_explorer/api.py +166 -0
- ai_model_explorer/cli.py +75 -0
- ai_model_explorer/js/infer.js +640 -0
- ai_model_explorer/js/loaders.js +162 -0
- ai_model_explorer/web/en/index.html +2742 -0
- ai_model_explorer/web/index.html +2742 -0
- ai_model_explorer/web/og.png +0 -0
- ai_model_explorer-0.1.0.dist-info/METADATA +72 -0
- ai_model_explorer-0.1.0.dist-info/RECORD +16 -0
- ai_model_explorer-0.1.0.dist-info/WHEEL +4 -0
- ai_model_explorer-0.1.0.dist-info/entry_points.txt +2 -0
- ai_model_explorer-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""AI Model Explorer: see any AI model's architecture from its weight manifest, without downloading the weights.
|
|
2
|
+
|
|
3
|
+
from ai_model_explorer import inspect, summary
|
|
4
|
+
a = inspect("Qwen/Qwen3-8B")
|
|
5
|
+
print(summary(a))
|
|
6
|
+
|
|
7
|
+
Method: S. Singkul, "Reading the Architecture off the Weights: Manifest-Only Recovery of Neural Network Anatomy", 2026.
|
|
8
|
+
"""
|
|
9
|
+
__version__ = "0.1.0"
|
|
10
|
+
|
|
11
|
+
from .api import inspect, summary # noqa: E402
|
|
12
|
+
|
|
13
|
+
__all__ = ["inspect", "summary", "__version__"]
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""Run the explorer's own JavaScript (loaders.js, infer.js) inside QuickJS, so Python gets exactly the website's result.
|
|
2
|
+
|
|
3
|
+
Python does the network I/O: the JS side calls a small fetch() shim that hands each request to Python. Binary bodies
|
|
4
|
+
cross the boundary as latin-1 strings (one char per byte), which QuickJS turns back into a Uint8Array.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from importlib import resources
|
|
10
|
+
from typing import Callable, Optional
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
import quickjs # type: ignore
|
|
14
|
+
except ImportError as e: # pragma: no cover
|
|
15
|
+
raise ImportError("ai-model-explorer needs the 'quickjs' package: pip install quickjs") from e
|
|
16
|
+
|
|
17
|
+
_PRELUDE = r"""
|
|
18
|
+
var window = undefined;
|
|
19
|
+
function TextDecoder() {}
|
|
20
|
+
TextDecoder.prototype.decode = function (u8) {
|
|
21
|
+
if (!u8) return "";
|
|
22
|
+
var src = u8.buffer && u8.buffer.__s;
|
|
23
|
+
if (src !== undefined && u8.byteOffset === 0 && u8.length === u8.buffer.byteLength) return __pyutf8(src);
|
|
24
|
+
var CH = 0x8000, parts = [];
|
|
25
|
+
for (var i = 0; i < u8.length; i += CH) parts.push(String.fromCharCode.apply(null, u8.subarray(i, i + CH)));
|
|
26
|
+
var s = parts.join("");
|
|
27
|
+
try { return decodeURIComponent(escape(s)); } catch (e) { return s; }
|
|
28
|
+
};
|
|
29
|
+
function __bytes(s) { var u = new Uint8Array(s.length); for (var i = 0; i < s.length; i++) u[i] = s.charCodeAt(i) & 255; return u; }
|
|
30
|
+
function fetch(url, opts) {
|
|
31
|
+
var range = opts && opts.headers && opts.headers.Range ? opts.headers.Range : "";
|
|
32
|
+
var r = JSON.parse(__pyfetch(url, range));
|
|
33
|
+
var body = r.body;
|
|
34
|
+
return Promise.resolve({
|
|
35
|
+
status: r.status, ok: r.status >= 200 && r.status < 300,
|
|
36
|
+
text: function () { return Promise.resolve(new TextDecoder().decode(__bytes(body))); },
|
|
37
|
+
json: function () { return Promise.resolve(JSON.parse(new TextDecoder().decode(__bytes(body)))); },
|
|
38
|
+
arrayBuffer: function () { var b = __bytes(body).buffer; b.__s = body; return Promise.resolve(b); }
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
var module = undefined;
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _js(name: str) -> str:
|
|
46
|
+
return resources.files("ai_model_explorer").joinpath("js", name).read_text(encoding="utf-8")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class Engine:
|
|
50
|
+
def __init__(self, fetch: Optional[Callable[[str, str], tuple]] = None):
|
|
51
|
+
self.ctx = quickjs.Context()
|
|
52
|
+
self.ctx.set_max_stack_size(8 * 1024 * 1024)
|
|
53
|
+
self._fetch = fetch
|
|
54
|
+
self.ctx.add_callable("__pyfetch", self._pyfetch)
|
|
55
|
+
self.ctx.add_callable("__pyget", self._pyget)
|
|
56
|
+
self.ctx.add_callable("__pyutf8", lambda s: s.encode("latin-1").decode("utf-8", "replace"))
|
|
57
|
+
self._inbox = {}
|
|
58
|
+
self.ctx.eval(_PRELUDE)
|
|
59
|
+
self.ctx.eval(_js("loaders.js"))
|
|
60
|
+
self.ctx.eval(_js("infer.js"))
|
|
61
|
+
|
|
62
|
+
# ---- host callbacks
|
|
63
|
+
def _pyfetch(self, url, rng):
|
|
64
|
+
if self._fetch is None:
|
|
65
|
+
return json.dumps({"status": 599, "body": ""})
|
|
66
|
+
status, body = self._fetch(url, rng or None)
|
|
67
|
+
return json.dumps({"status": status, "body": body.decode("latin-1")})
|
|
68
|
+
|
|
69
|
+
def _pyget(self, key):
|
|
70
|
+
return self._inbox.pop(key)
|
|
71
|
+
|
|
72
|
+
def _put(self, key, obj) -> str:
|
|
73
|
+
self._inbox[key] = json.dumps(obj)
|
|
74
|
+
return f'JSON.parse(__pyget("{key}"))'
|
|
75
|
+
|
|
76
|
+
def _await(self, expr: str):
|
|
77
|
+
"""Evaluate an async JS expression to completion and return its JSON value."""
|
|
78
|
+
self.ctx.eval(
|
|
79
|
+
"var __res = undefined, __err = undefined;"
|
|
80
|
+
f"Promise.resolve({expr}).then(function (v) {{ __res = JSON.stringify(v === undefined ? null : v); }},"
|
|
81
|
+
" function (e) { __err = String(e && e.message || e); });")
|
|
82
|
+
while self.ctx.execute_pending_job():
|
|
83
|
+
pass
|
|
84
|
+
err = self.ctx.eval("__err")
|
|
85
|
+
if err is not None:
|
|
86
|
+
raise RuntimeError(err)
|
|
87
|
+
res = self.ctx.eval("__res")
|
|
88
|
+
if res is None:
|
|
89
|
+
raise RuntimeError("JavaScript promise did not settle")
|
|
90
|
+
return json.loads(res)
|
|
91
|
+
|
|
92
|
+
# ---- API
|
|
93
|
+
def hf_manifest(self, repo: str, files: list, opt: dict):
|
|
94
|
+
return self._await(f'Loaders.hfManifest({json.dumps(repo)}, {self._put("files", files)}, function () {{}}, {self._put("opt", opt)})')
|
|
95
|
+
|
|
96
|
+
def infer(self, tensors: dict, lex: bool = True) -> dict:
|
|
97
|
+
return self._await(f'ArchInfer.infer({self._put("t", tensors)}, {{lex: {"true" if lex else "false"}}})')
|
|
98
|
+
|
|
99
|
+
def fuse(self, anatomy: dict, config: dict):
|
|
100
|
+
return self._await(f'ArchInfer.fuseConfig({self._put("a", anatomy)}, {self._put("c", config)})')
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Minimal HTTP for Hugging Face: file list, small JSON files, and byte ranges. Standard library only."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import urllib.error
|
|
8
|
+
import urllib.request
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from . import __version__
|
|
12
|
+
|
|
13
|
+
ENDPOINT = os.environ.get("HF_ENDPOINT", "https://huggingface.co").rstrip("/")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Hub:
|
|
17
|
+
def __init__(self, token: Optional[str] = None, timeout: float = 60.0):
|
|
18
|
+
self.token = token or os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
|
|
19
|
+
self.timeout = timeout
|
|
20
|
+
self.bytes_read = 0
|
|
21
|
+
|
|
22
|
+
def get(self, url: str, rng: Optional[str] = None):
|
|
23
|
+
"""Return (status, body bytes). Never raises for HTTP errors; the caller decides."""
|
|
24
|
+
url = url.replace("https://huggingface.co", ENDPOINT, 1)
|
|
25
|
+
h = {"User-Agent": f"ai-model-explorer/{__version__}"}
|
|
26
|
+
if rng:
|
|
27
|
+
h["Range"] = rng
|
|
28
|
+
if self.token and url.startswith(ENDPOINT):
|
|
29
|
+
h["Authorization"] = f"Bearer {self.token}"
|
|
30
|
+
try:
|
|
31
|
+
with urllib.request.urlopen(urllib.request.Request(url, headers=h), timeout=self.timeout) as r:
|
|
32
|
+
body = r.read()
|
|
33
|
+
self.bytes_read += len(body)
|
|
34
|
+
return r.status, body
|
|
35
|
+
except urllib.error.HTTPError as e:
|
|
36
|
+
return e.code, e.read() or b""
|
|
37
|
+
|
|
38
|
+
def json(self, url: str):
|
|
39
|
+
st, body = self.get(url)
|
|
40
|
+
if st in (401, 403):
|
|
41
|
+
raise PermissionError("gated or private repository: accept its license on Hugging Face and pass a token (HF_TOKEN)")
|
|
42
|
+
if st == 404:
|
|
43
|
+
m = re.search(r"/api/models/([^?]+)", url)
|
|
44
|
+
raise FileNotFoundError(f"repository not found: {m.group(1)}" if m else f"not found: {url}")
|
|
45
|
+
if st >= 400:
|
|
46
|
+
raise RuntimeError(f"HTTP {st} for {url}")
|
|
47
|
+
return json.loads(body.decode("utf-8"))
|
|
48
|
+
|
|
49
|
+
def model_info(self, repo: str) -> dict:
|
|
50
|
+
return self.json(f"https://huggingface.co/api/models/{repo}?blobs=true")
|
ai_model_explorer/api.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""inspect(): the anatomy of a model from its weight manifest, for a Hugging Face repo or local safetensors files."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import struct
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional, Union
|
|
10
|
+
|
|
11
|
+
from ._engine import Engine
|
|
12
|
+
from ._hub import Hub
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _local_header(path: Path) -> tuple[dict, int]:
|
|
16
|
+
with open(path, "rb") as f:
|
|
17
|
+
n = struct.unpack("<Q", f.read(8))[0]
|
|
18
|
+
if not 0 < n < 200_000_000:
|
|
19
|
+
raise ValueError(f"{path} is not a safetensors file")
|
|
20
|
+
h = json.loads(f.read(n).decode("utf-8"))
|
|
21
|
+
h.pop("__metadata__", None)
|
|
22
|
+
return h, 8 + n
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _local(path: Path):
|
|
26
|
+
files = [path] if path.is_file() else sorted(p for p in path.rglob("*.safetensors") if not re.search(r"(^|/)(optimizer|adapter[._])", str(p)))
|
|
27
|
+
if not files:
|
|
28
|
+
raise FileNotFoundError(f"no .safetensors file under {path}")
|
|
29
|
+
tensors, read = {}, 0
|
|
30
|
+
base = path if path.is_dir() else path.parent
|
|
31
|
+
for p in files:
|
|
32
|
+
h, b = _local_header(p)
|
|
33
|
+
read += b
|
|
34
|
+
rel = p.relative_to(base).parent.as_posix()
|
|
35
|
+
prefix = "" if rel in ("", ".") else rel.replace("/", ".") + "."
|
|
36
|
+
for k, v in h.items():
|
|
37
|
+
tensors[prefix + k] = {"dtype": v["dtype"], "shape": v["shape"]}
|
|
38
|
+
cfg = None
|
|
39
|
+
c = base / "config.json"
|
|
40
|
+
if c.exists():
|
|
41
|
+
cfg = json.loads(c.read_text(encoding="utf-8"))
|
|
42
|
+
return tensors, cfg, read, len(files), None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _remote(repo: str, hub: Hub):
|
|
46
|
+
info = hub.model_info(repo)
|
|
47
|
+
files = [{"n": s["rfilename"], "size": s.get("size") or 0} for s in info.get("siblings", [])]
|
|
48
|
+
names = {f["n"] for f in files}
|
|
49
|
+
base = f"https://huggingface.co/{repo}/resolve/main/"
|
|
50
|
+
cfg = mi = None
|
|
51
|
+
if "config.json" in names:
|
|
52
|
+
try:
|
|
53
|
+
cfg = hub.json(base + "config.json")
|
|
54
|
+
except Exception:
|
|
55
|
+
cfg = None
|
|
56
|
+
if "model_index.json" in names:
|
|
57
|
+
try:
|
|
58
|
+
mi = hub.json(base + "model_index.json")
|
|
59
|
+
except Exception:
|
|
60
|
+
mi = None
|
|
61
|
+
opt = {"prefer": (cfg or {}).get("default_size"),
|
|
62
|
+
"only": [k for k, v in (mi or {}).items() if not k.startswith("_") and isinstance(v, list) and v and v[0]] if mi else None}
|
|
63
|
+
eng = Engine(fetch=hub.get)
|
|
64
|
+
mf = eng.hf_manifest(repo, files, opt)
|
|
65
|
+
if not mf:
|
|
66
|
+
raise FileNotFoundError(f"{repo}: no .safetensors or PyTorch checkpoint found")
|
|
67
|
+
weight_bytes = sum(f["size"] for f in files)
|
|
68
|
+
return mf["tensors"], cfg, mf["read"], mf["nfiles"], {"engine": eng, "weight_bytes": weight_bytes,
|
|
69
|
+
"downloads": info.get("downloads"), "hub_params": (info.get("safetensors") or {}).get("total")}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def inspect(model: Union[str, os.PathLike], token: Optional[str] = None, config: Optional[dict] = None,
|
|
73
|
+
include_tensors: bool = False) -> dict:
|
|
74
|
+
"""Recover a model's anatomy without downloading its weights.
|
|
75
|
+
|
|
76
|
+
model: a Hugging Face repo id ("Qwen/Qwen3-8B", or a huggingface.co URL), a .safetensors file, or a folder of them.
|
|
77
|
+
token: Hugging Face token for gated or private repos (default: $HF_TOKEN).
|
|
78
|
+
config: a config.json dict to fuse instead of the repo's own.
|
|
79
|
+
Returns a JSON-serializable dict: family, totals, hyperparameters, components with their layer stacks and block
|
|
80
|
+
types, edges between components, quantization, the config check, and how many bytes were read.
|
|
81
|
+
"""
|
|
82
|
+
s = str(model)
|
|
83
|
+
if os.path.exists(s):
|
|
84
|
+
tensors, cfg, read, nfiles, extra = _local(Path(s))
|
|
85
|
+
eng = Engine()
|
|
86
|
+
source = {"kind": "local", "path": str(Path(s).resolve())}
|
|
87
|
+
else:
|
|
88
|
+
m = re.search(r"(?:huggingface\.co/)?([\w.-]+/[\w.-]+)", s)
|
|
89
|
+
if not m:
|
|
90
|
+
raise ValueError("expected a Hugging Face repo id (owner/name) or a path to .safetensors")
|
|
91
|
+
repo = re.sub(r"/(tree|blob|resolve)/.*$", "", m.group(1))
|
|
92
|
+
tensors, cfg, read, nfiles, extra = _remote(repo, Hub(token))
|
|
93
|
+
eng = extra.pop("engine")
|
|
94
|
+
source = {"kind": "huggingface", "repo": repo, **{k: v for k, v in extra.items() if v is not None}}
|
|
95
|
+
if config is not None:
|
|
96
|
+
cfg = config
|
|
97
|
+
anatomy = eng.infer(tensors)
|
|
98
|
+
check = eng.fuse(anatomy, cfg) if cfg else None
|
|
99
|
+
out = {
|
|
100
|
+
"source": source,
|
|
101
|
+
"files_read": nfiles,
|
|
102
|
+
"header_bytes": read,
|
|
103
|
+
"tensor_count": len(tensors),
|
|
104
|
+
"family": anatomy.get("family"),
|
|
105
|
+
"totals": anatomy.get("totals"),
|
|
106
|
+
"hyperparameters": anatomy.get("hp"),
|
|
107
|
+
"quantization": anatomy.get("quant"),
|
|
108
|
+
"components": anatomy.get("comps"),
|
|
109
|
+
"edges": anatomy.get("edges"),
|
|
110
|
+
"evidence": anatomy.get("evidence"),
|
|
111
|
+
"config_check": check,
|
|
112
|
+
"has_config": cfg is not None,
|
|
113
|
+
}
|
|
114
|
+
if include_tensors:
|
|
115
|
+
out["tensors"] = anatomy.get("tensors")
|
|
116
|
+
return out
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _fmt_n(n) -> str:
|
|
120
|
+
if n is None:
|
|
121
|
+
return "?"
|
|
122
|
+
for u, k in (("T", 1e12), ("B", 1e9), ("M", 1e6), ("K", 1e3)):
|
|
123
|
+
if n >= k:
|
|
124
|
+
return f"{n / k:.2f}{u}".replace(".00", "")
|
|
125
|
+
return str(n)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _fmt_b(n) -> str:
|
|
129
|
+
if n is None:
|
|
130
|
+
return "?"
|
|
131
|
+
for u, k in (("TB", 1e12), ("GB", 1e9), ("MB", 1e6), ("KB", 1e3)):
|
|
132
|
+
if n >= k:
|
|
133
|
+
return f"{n / k:.1f} {u}"
|
|
134
|
+
return f"{n} B"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def summary(a: dict) -> str:
|
|
138
|
+
"""A short human-readable report of an inspect() result."""
|
|
139
|
+
src = a["source"]
|
|
140
|
+
name = src.get("repo") or src.get("path")
|
|
141
|
+
t = a.get("totals") or {}
|
|
142
|
+
hp = a.get("hyperparameters") or {}
|
|
143
|
+
lines = [f"{name}",
|
|
144
|
+
f" family {a.get('family')} · {_fmt_n(t.get('params'))} parameters · {_fmt_b(t.get('bytes'))} of weights"
|
|
145
|
+
+ (f" · stored as {', '.join(a['quantization'])}" if a.get("quantization") else ""),
|
|
146
|
+
f" read {_fmt_b(a.get('header_bytes'))} from {a.get('files_read')} file(s), {a.get('tensor_count')} tensors"]
|
|
147
|
+
keys = [("d", "width"), ("layers", "layers"), ("heads", "heads"), ("kv_heads", "kv heads"), ("head_dim", "head dim"),
|
|
148
|
+
("ffn", "ffn"), ("experts", "experts"), ("vocab", "vocab")]
|
|
149
|
+
hps = " · ".join(f"{lab} {hp[k]}" for k, lab in keys if hp.get(k))
|
|
150
|
+
if hps:
|
|
151
|
+
lines.append(" " + hps)
|
|
152
|
+
lines.append(" components:")
|
|
153
|
+
for c in a.get("components") or []:
|
|
154
|
+
role = c.get("role") or ""
|
|
155
|
+
head = f" {c.get('id')}" + (f" ({role})" if role else "") + (f" · d={c['d']}" if c.get("d") else "")
|
|
156
|
+
lines.append(head)
|
|
157
|
+
for st in c.get("stacks") or []:
|
|
158
|
+
types = ", ".join(f"{ty.get('label')}" + (f" ×{ty['n']}" if ty.get("n") and len(st.get("types", [])) > 1 else "") for ty in st.get("types") or [])
|
|
159
|
+
lines.append(f" {st.get('path')}: {st.get('n')} blocks · {types}")
|
|
160
|
+
for e in a.get("edges") or []:
|
|
161
|
+
lines.append(f" {e.get('from')} → {e.get('to')}" + (f" (via {e['via']})" if e.get("via") else ""))
|
|
162
|
+
chk = a.get("config_check")
|
|
163
|
+
if chk:
|
|
164
|
+
bad = [k for k, v in chk.items() if isinstance(v, dict) and v.get("checked") and v.get("consistent") is False]
|
|
165
|
+
lines.append(" config.json: " + ("agrees with the weights" if not bad else "disagrees with the weights for " + ", ".join(bad)))
|
|
166
|
+
return "\n".join(lines)
|
ai_model_explorer/cli.py
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""Command line: `ai-model-explorer open [REPO]` starts the web app locally; `ai-model-explorer inspect REPO` prints the anatomy."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import functools
|
|
6
|
+
import http.server
|
|
7
|
+
import json
|
|
8
|
+
import socketserver
|
|
9
|
+
import sys
|
|
10
|
+
import threading
|
|
11
|
+
import urllib.parse
|
|
12
|
+
import webbrowser
|
|
13
|
+
from importlib import resources
|
|
14
|
+
|
|
15
|
+
from . import __version__
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _serve(repo: str | None, port: int, lang: str, no_browser: bool) -> None:
|
|
19
|
+
web = resources.files("ai_model_explorer").joinpath("web")
|
|
20
|
+
with resources.as_file(web) as root:
|
|
21
|
+
handler = functools.partial(http.server.SimpleHTTPRequestHandler, directory=str(root))
|
|
22
|
+
handler.log_message = lambda *a, **k: None
|
|
23
|
+
with socketserver.TCPServer(("127.0.0.1", port), handler) as httpd:
|
|
24
|
+
url = f"http://127.0.0.1:{httpd.server_address[1]}/" + ("en/" if lang == "en" else "")
|
|
25
|
+
if repo:
|
|
26
|
+
url += "#hf=" + urllib.parse.quote(repo, safe="/")
|
|
27
|
+
print(f"AI Model Explorer {__version__} running at {url} (Ctrl+C to stop)")
|
|
28
|
+
if not no_browser:
|
|
29
|
+
threading.Timer(0.4, lambda: webbrowser.open(url)).start()
|
|
30
|
+
try:
|
|
31
|
+
httpd.serve_forever()
|
|
32
|
+
except KeyboardInterrupt:
|
|
33
|
+
print()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main(argv=None) -> int:
|
|
37
|
+
p = argparse.ArgumentParser(prog="ai-model-explorer",
|
|
38
|
+
description="See any AI model's architecture from its safetensors header, without downloading weights.")
|
|
39
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
40
|
+
sub = p.add_subparsers(dest="cmd")
|
|
41
|
+
|
|
42
|
+
o = sub.add_parser("open", help="open the web app in your browser (runs locally, works offline for files)")
|
|
43
|
+
o.add_argument("repo", nargs="?", help="Hugging Face repo to load right away, e.g. Qwen/Qwen3-8B")
|
|
44
|
+
o.add_argument("--port", type=int, default=0, help="port (default: any free port)")
|
|
45
|
+
o.add_argument("--lang", choices=["th", "en"], default="en")
|
|
46
|
+
o.add_argument("--no-browser", action="store_true", help="only print the URL")
|
|
47
|
+
|
|
48
|
+
i = sub.add_parser("inspect", help="print the anatomy of a Hugging Face repo or local .safetensors")
|
|
49
|
+
i.add_argument("model", help="repo id (owner/name), huggingface.co URL, .safetensors file or folder")
|
|
50
|
+
i.add_argument("--json", action="store_true", help="print the full result as JSON")
|
|
51
|
+
i.add_argument("--tensors", action="store_true", help="with --json: include the role of every tensor")
|
|
52
|
+
i.add_argument("--token", help="Hugging Face token for gated/private repos (default: $HF_TOKEN)")
|
|
53
|
+
i.add_argument("--config", help="path to a config.json to use instead of the repo's own")
|
|
54
|
+
|
|
55
|
+
a = p.parse_args(argv)
|
|
56
|
+
if a.cmd in (None, "open"):
|
|
57
|
+
_serve(getattr(a, "repo", None), getattr(a, "port", 0), getattr(a, "lang", "en"), getattr(a, "no_browser", False))
|
|
58
|
+
return 0
|
|
59
|
+
|
|
60
|
+
from .api import inspect, summary
|
|
61
|
+
cfg = None
|
|
62
|
+
if a.config:
|
|
63
|
+
with open(a.config, encoding="utf-8") as f:
|
|
64
|
+
cfg = json.load(f)
|
|
65
|
+
try:
|
|
66
|
+
r = inspect(a.model, token=a.token, config=cfg, include_tensors=a.tensors)
|
|
67
|
+
except (PermissionError, FileNotFoundError, ValueError, RuntimeError) as e:
|
|
68
|
+
print(f"error: {e}", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
print(json.dumps(r, indent=1, ensure_ascii=False) if a.json else summary(r))
|
|
71
|
+
return 0
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
if __name__ == "__main__":
|
|
75
|
+
sys.exit(main())
|