laboard 1.0.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.
- laboard/__init__.py +1 -0
- laboard/__main__.py +5 -0
- laboard/app.py +528 -0
- laboard/assets/fonts/Figtree-Black.ttf +0 -0
- laboard/assets/fonts/Figtree-Regular.ttf +0 -0
- laboard/assets/fonts/Inter-latin-var.woff2 +0 -0
- laboard/assets/logo/laboard_splash.svg +2 -0
- laboard/charts.py +631 -0
- laboard/labels.py +189 -0
- laboard/modelinfo.py +218 -0
- laboard/netronview.py +107 -0
- laboard/newproject.py +287 -0
- laboard/pages/__init__.py +0 -0
- laboard/pages/data.py +352 -0
- laboard/pages/home.py +242 -0
- laboard/pages/hyperparameter.py +154 -0
- laboard/pages/model.py +78 -0
- laboard/pages/result.py +707 -0
- laboard/pages/setting.py +272 -0
- laboard/project.py +1127 -0
- laboard/readers.py +156 -0
- laboard/remote.py +343 -0
- laboard/sidebar.py +242 -0
- laboard/store.py +38 -0
- laboard/theme.py +111 -0
- laboard/tomledit.py +262 -0
- laboard/torchgraph.py +102 -0
- laboard/widgets.py +1128 -0
- laboard-1.0.0.dist-info/METADATA +400 -0
- laboard-1.0.0.dist-info/RECORD +34 -0
- laboard-1.0.0.dist-info/WHEEL +5 -0
- laboard-1.0.0.dist-info/entry_points.txt +2 -0
- laboard-1.0.0.dist-info/licenses/LICENSE +8 -0
- laboard-1.0.0.dist-info/top_level.txt +1 -0
laboard/labels.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import zlib
|
|
3
|
+
from collections import Counter
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
|
|
6
|
+
from PyQt6.QtCore import QPointF, QRectF, Qt
|
|
7
|
+
from PyQt6.QtGui import QBitmap, QColor, QImage, QPainter, QPainterPath, QPen, QPixmap, QRegion, QTransform
|
|
8
|
+
|
|
9
|
+
from . import remote
|
|
10
|
+
from .theme import font
|
|
11
|
+
|
|
12
|
+
PALETTE = ("#ff5c5c", "#ffb020", "#f7e14b", "#7bd84a", "#2ec4b6", "#3fa7ff", "#7c6cff", "#d45cff", "#ff6fb1",
|
|
13
|
+
"#00e0a0", "#ff8a3d", "#9fe870")
|
|
14
|
+
COCO_SKELETON = ((16, 14), (14, 12), (17, 15), (15, 13), (12, 13), (6, 12), (7, 13), (6, 7), (6, 8), (7, 9), (8, 10),
|
|
15
|
+
(9, 11), (2, 3), (1, 2), (1, 3), (2, 4), (3, 5), (4, 6), (5, 7))
|
|
16
|
+
MAX_TEXT = 60
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@lru_cache(maxsize=256)
|
|
20
|
+
def _loads(text):
|
|
21
|
+
try:
|
|
22
|
+
return json.loads(text)
|
|
23
|
+
except ValueError:
|
|
24
|
+
return text
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse(value):
|
|
28
|
+
if value is None or isinstance(value, (list, dict)):
|
|
29
|
+
return value
|
|
30
|
+
v = _loads(str(value)) if str(value)[:1] in "[{" else value
|
|
31
|
+
return v if isinstance(v, (list, dict)) else value
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _objects(v):
|
|
35
|
+
if isinstance(v, dict):
|
|
36
|
+
v = v.get("segments_info", [])
|
|
37
|
+
return [o for o in v if isinstance(o, dict)] if isinstance(v, list) else []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def categories(value):
|
|
41
|
+
v = parse(value)
|
|
42
|
+
if v is None:
|
|
43
|
+
return []
|
|
44
|
+
if not isinstance(v, (list, dict)):
|
|
45
|
+
return [str(v)]
|
|
46
|
+
if isinstance(v, list) and all(not isinstance(x, (list, dict)) for x in v):
|
|
47
|
+
return [str(x) for x in v]
|
|
48
|
+
return [str(o["category"]) for o in _objects(v) if o.get("category") is not None]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def summary(value):
|
|
52
|
+
v = parse(value)
|
|
53
|
+
if v is None:
|
|
54
|
+
return None
|
|
55
|
+
if not isinstance(v, (list, dict)):
|
|
56
|
+
text = str(v)
|
|
57
|
+
elif isinstance(v, list) and all(not isinstance(x, (list, dict)) for x in v):
|
|
58
|
+
text = ", ".join(map(str, v)) or "none"
|
|
59
|
+
else:
|
|
60
|
+
objs = _objects(v)
|
|
61
|
+
counts = Counter(str(o.get("category", "?")) for o in objs)
|
|
62
|
+
what = ("segment" if isinstance(v, dict) else "object") + ("" if len(objs) == 1 else "s")
|
|
63
|
+
names = ", ".join(c if n == 1 else f"{c} ×{n}" for c, n in counts.most_common())
|
|
64
|
+
text = f"{len(objs)} {what}" + (f" · {names}" if names else "")
|
|
65
|
+
return text if len(text) <= MAX_TEXT else text[:MAX_TEXT - 1] + "…"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def color(category):
|
|
69
|
+
return PALETTE[zlib.crc32(str(category).encode()) % len(PALETTE)]
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _polygons(seg):
|
|
73
|
+
path = QPainterPath()
|
|
74
|
+
path.setFillRule(Qt.FillRule.WindingFill)
|
|
75
|
+
if isinstance(seg, list):
|
|
76
|
+
for poly in seg:
|
|
77
|
+
if isinstance(poly, list) and len(poly) >= 6:
|
|
78
|
+
path.moveTo(poly[0], poly[1])
|
|
79
|
+
for i in range(2, len(poly) - 1, 2):
|
|
80
|
+
path.lineTo(poly[i], poly[i + 1])
|
|
81
|
+
path.closeSubpath()
|
|
82
|
+
elif isinstance(seg, dict) and isinstance(seg.get("counts"), list) and seg.get("size"):
|
|
83
|
+
h = seg["size"][0]
|
|
84
|
+
at = 0
|
|
85
|
+
for i, n in enumerate(seg["counts"]):
|
|
86
|
+
if i % 2:
|
|
87
|
+
start, end = at, at + n
|
|
88
|
+
while start < end:
|
|
89
|
+
x, y = divmod(start, h)
|
|
90
|
+
run = min(end - start, h - y)
|
|
91
|
+
path.addRect(QRectF(x, y, 1, run))
|
|
92
|
+
start += run
|
|
93
|
+
at += n
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _panoptic(path, segments):
|
|
98
|
+
ids = QImage(path).convertToFormat(QImage.Format.Format_RGB32)
|
|
99
|
+
if ids.isNull():
|
|
100
|
+
return None
|
|
101
|
+
out = QImage(ids.size(), QImage.Format.Format_ARGB32_Premultiplied)
|
|
102
|
+
out.fill(Qt.GlobalColor.transparent)
|
|
103
|
+
p = QPainter(out)
|
|
104
|
+
for seg in segments:
|
|
105
|
+
sid = seg.get("id")
|
|
106
|
+
if not isinstance(sid, int):
|
|
107
|
+
continue
|
|
108
|
+
mask = ids.createMaskFromColor(QColor(sid % 256, sid // 256 % 256, sid // 65536 % 256).rgb(), Qt.MaskMode.MaskOutColor)
|
|
109
|
+
fill = QColor(color(seg.get("category", "")))
|
|
110
|
+
fill.setAlpha(110)
|
|
111
|
+
p.setClipRegion(QRegion(QBitmap.fromImage(mask)))
|
|
112
|
+
p.fillRect(out.rect(), fill)
|
|
113
|
+
p.end()
|
|
114
|
+
return QPixmap.fromImage(out)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Overlay:
|
|
118
|
+
def __init__(self, value):
|
|
119
|
+
v = parse(value)
|
|
120
|
+
self.shapes = []
|
|
121
|
+
self.mask = None
|
|
122
|
+
if isinstance(v, dict) and v.get("mask_path"):
|
|
123
|
+
self.mask = _panoptic(remote.local(str(v["mask_path"])), _objects(v))
|
|
124
|
+
for o in _objects(v):
|
|
125
|
+
cat = str(o.get("category", ""))
|
|
126
|
+
if isinstance(v, dict):
|
|
127
|
+
path = QPainterPath()
|
|
128
|
+
if isinstance(o.get("bbox"), list) and len(o["bbox"]) == 4:
|
|
129
|
+
path.addRect(QRectF(*o["bbox"]))
|
|
130
|
+
self.shapes.append((cat, path, None, [], []))
|
|
131
|
+
continue
|
|
132
|
+
path, filled = QPainterPath(), False
|
|
133
|
+
if o.get("segmentation") is not None:
|
|
134
|
+
path, filled = _polygons(o["segmentation"]), True
|
|
135
|
+
elif isinstance(o.get("bbox"), list) and len(o["bbox"]) == 4:
|
|
136
|
+
path.addRect(QRectF(*o["bbox"]))
|
|
137
|
+
points, limbs = [], []
|
|
138
|
+
kp = o.get("keypoints")
|
|
139
|
+
if isinstance(kp, list) and len(kp) >= 3:
|
|
140
|
+
points = [(kp[i], kp[i + 1]) if kp[i + 2] else None for i in range(0, len(kp) - 2, 3)]
|
|
141
|
+
limbs = [(a - 1, b - 1) for a, b in COCO_SKELETON] if len(points) == 17 else []
|
|
142
|
+
self.shapes.append((cat, path, filled, points, limbs))
|
|
143
|
+
|
|
144
|
+
def __bool__(self):
|
|
145
|
+
return bool(self.shapes) or self.mask is not None
|
|
146
|
+
|
|
147
|
+
def paint(self, p, rect, scale):
|
|
148
|
+
p.save()
|
|
149
|
+
to_view = QTransform(scale, 0, 0, scale, rect.x(), rect.y())
|
|
150
|
+
if self.mask is not None:
|
|
151
|
+
p.drawPixmap(rect, self.mask, QRectF(self.mask.rect()))
|
|
152
|
+
names = []
|
|
153
|
+
for cat, path, filled, points, limbs in self.shapes:
|
|
154
|
+
c = QColor(color(cat))
|
|
155
|
+
view = to_view.map(path)
|
|
156
|
+
if filled is not None and not path.isEmpty():
|
|
157
|
+
if filled:
|
|
158
|
+
fill = QColor(c)
|
|
159
|
+
fill.setAlpha(70)
|
|
160
|
+
p.fillPath(view, fill)
|
|
161
|
+
p.setPen(QPen(c, 2))
|
|
162
|
+
p.setBrush(Qt.BrushStyle.NoBrush)
|
|
163
|
+
p.drawPath(view)
|
|
164
|
+
if points:
|
|
165
|
+
p.setPen(QPen(c, 2))
|
|
166
|
+
for a, b in limbs:
|
|
167
|
+
if points[a] and points[b]:
|
|
168
|
+
p.drawLine(to_view.map(QPointF(*points[a])), to_view.map(QPointF(*points[b])))
|
|
169
|
+
p.setPen(QPen(QColor("#ffffff"), 1))
|
|
170
|
+
p.setBrush(c)
|
|
171
|
+
for pt in points:
|
|
172
|
+
if pt:
|
|
173
|
+
p.drawEllipse(to_view.map(QPointF(*pt)), 3.5, 3.5)
|
|
174
|
+
if cat and not path.isEmpty():
|
|
175
|
+
box = view.boundingRect()
|
|
176
|
+
names.append((cat, c, QPointF(box.x(), box.y()) if filled is not None else box.center()))
|
|
177
|
+
p.setFont(font(11, 600))
|
|
178
|
+
fm = p.fontMetrics()
|
|
179
|
+
for cat, c, at in names:
|
|
180
|
+
w = fm.horizontalAdvance(cat) + 10
|
|
181
|
+
chip = QRectF(at.x(), at.y() - 18, w, 18) if at.y() >= rect.y() + 18 else QRectF(at.x(), at.y(), w, 18)
|
|
182
|
+
if self.mask is not None:
|
|
183
|
+
chip.moveCenter(at)
|
|
184
|
+
p.setPen(Qt.PenStyle.NoPen)
|
|
185
|
+
p.setBrush(c)
|
|
186
|
+
p.drawRoundedRect(chip, 4, 4)
|
|
187
|
+
p.setPen(QColor("#0e0f0c"))
|
|
188
|
+
p.drawText(chip, Qt.AlignmentFlag.AlignCenter, cat)
|
|
189
|
+
p.restore()
|
laboard/modelinfo.py
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import math
|
|
3
|
+
import pickle
|
|
4
|
+
import re
|
|
5
|
+
import struct
|
|
6
|
+
import zipfile
|
|
7
|
+
from collections import Counter, OrderedDict
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
MAX_LAYERS = 12
|
|
11
|
+
_cache = {}
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Tensor:
|
|
15
|
+
__slots__ = ("shape", "dtype")
|
|
16
|
+
|
|
17
|
+
def __init__(self, shape, dtype):
|
|
18
|
+
self.shape, self.dtype = tuple(int(d) for d in shape), str(dtype)
|
|
19
|
+
|
|
20
|
+
@property
|
|
21
|
+
def size(self):
|
|
22
|
+
return math.prod(self.shape)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def count(n):
|
|
26
|
+
return f"{n / 1e9:.2f}B" if n >= 1e9 else f"{n / 1e6:.2f}M" if n >= 1e6 else f"{n / 1e3:.1f}K" if n >= 1e3 else str(n)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _onnx(p):
|
|
30
|
+
import onnx
|
|
31
|
+
|
|
32
|
+
m = onnx.load(str(p), load_external_data=False)
|
|
33
|
+
g = m.graph
|
|
34
|
+
dtype = onnx.TensorProto.DataType.Name
|
|
35
|
+
tensors = {t.name: Tensor(t.dims, dtype(t.data_type).lower()) for t in g.initializer}
|
|
36
|
+
for n in g.node:
|
|
37
|
+
if n.op_type == "Constant":
|
|
38
|
+
for a in n.attribute:
|
|
39
|
+
if a.name == "value" and a.t.dims:
|
|
40
|
+
tensors.setdefault(n.output[0], Tensor(a.t.dims, dtype(a.t.data_type).lower()))
|
|
41
|
+
init = set(tensors)
|
|
42
|
+
users = {}
|
|
43
|
+
for n in g.node:
|
|
44
|
+
for i in n.input:
|
|
45
|
+
if i in init and i not in users:
|
|
46
|
+
users[i] = n.name.strip("/").rsplit("/", 1)[0] if "/" in n.name.strip("/") else n.op_type
|
|
47
|
+
tensors = {f"{users.get(k, 'unused')}:{k}": t for k, t in tensors.items()}
|
|
48
|
+
|
|
49
|
+
def shape(v):
|
|
50
|
+
dims = [d.dim_value or d.dim_param or "?" for d in v.type.tensor_type.shape.dim]
|
|
51
|
+
return f"{dtype(v.type.tensor_type.elem_type).lower()}[{','.join(map(str, dims))}]"
|
|
52
|
+
|
|
53
|
+
ops = Counter(n.op_type for n in g.node)
|
|
54
|
+
props = [("Format", f"ONNX IR v{m.ir_version}"), ("Producer", f"{m.producer_name} {m.producer_version}".strip() or "—"),
|
|
55
|
+
("Opset", ", ".join(f"{o.domain or 'ai.onnx'} {o.version}" for o in m.opset_import)),
|
|
56
|
+
*[("Input", f"{v.name}: {shape(v)}") for v in g.input if v.name not in init],
|
|
57
|
+
*[("Output", f"{v.name}: {shape(v)}") for v in g.output],
|
|
58
|
+
("Nodes", f"{len(g.node):,}"), ("Top ops", ", ".join(f"{k} {n}" for k, n in ops.most_common(4)))]
|
|
59
|
+
if any(t.data_location == onnx.TensorProto.EXTERNAL for t in g.initializer):
|
|
60
|
+
props.append(("Weights", "external data file (not loaded)"))
|
|
61
|
+
return tensors, props
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _safetensors(p):
|
|
65
|
+
with open(p, "rb") as f:
|
|
66
|
+
n = struct.unpack("<Q", f.read(8))[0]
|
|
67
|
+
header = json.loads(f.read(n))
|
|
68
|
+
meta = header.pop("__metadata__", None) or {}
|
|
69
|
+
tensors = {k: Tensor(v["shape"], v["dtype"].lower()) for k, v in header.items()}
|
|
70
|
+
props = [("Format", "safetensors"), ("Tensors", f"{len(tensors):,}"),
|
|
71
|
+
("Dtypes", ", ".join(sorted({t.dtype for t in tensors.values()})))]
|
|
72
|
+
props += [(f"meta · {k}", str(v)[:60]) for k, v in list(meta.items())[:4]]
|
|
73
|
+
return tensors, props
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _msgpack(p):
|
|
77
|
+
import msgpack
|
|
78
|
+
|
|
79
|
+
def ext(code, data):
|
|
80
|
+
if code == 1:
|
|
81
|
+
shape, dtype, _ = msgpack.unpackb(data, raw=False)
|
|
82
|
+
return Tensor(shape, dtype)
|
|
83
|
+
if code == 3:
|
|
84
|
+
return Tensor((), msgpack.unpackb(data, raw=False)[0])
|
|
85
|
+
return msgpack.ExtType(code, data)
|
|
86
|
+
|
|
87
|
+
with open(p, "rb") as f:
|
|
88
|
+
tree = msgpack.unpackb(f.read(), ext_hook=ext, raw=False, strict_map_key=False)
|
|
89
|
+
tensors = dict(_leaves(tree))
|
|
90
|
+
return tensors, [("Format", "Flax msgpack"), ("Tensors", f"{len(tensors):,}"),
|
|
91
|
+
("Dtypes", ", ".join(sorted({t.dtype for t in tensors.values()})))]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class _Obj:
|
|
95
|
+
def __new__(cls, *a, **k):
|
|
96
|
+
return object.__new__(cls)
|
|
97
|
+
|
|
98
|
+
def __init__(self, *a, **k):
|
|
99
|
+
self.args = a
|
|
100
|
+
|
|
101
|
+
def __setstate__(self, state):
|
|
102
|
+
self.state = state
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _Unpickler(pickle.Unpickler):
|
|
106
|
+
def find_class(self, module, name):
|
|
107
|
+
if module == "collections" and name == "OrderedDict":
|
|
108
|
+
return OrderedDict
|
|
109
|
+
if module == "torch._utils" and name.startswith("_rebuild_tensor"):
|
|
110
|
+
return lambda storage, offset, size, *a, **k: Tensor(size, storage)
|
|
111
|
+
if module == "torch._utils" and name == "_rebuild_parameter":
|
|
112
|
+
return lambda data, *a, **k: data
|
|
113
|
+
if module == "torch" and name.endswith("Storage"):
|
|
114
|
+
return name[:-len("Storage")].lower() or "untyped"
|
|
115
|
+
if module == "torch" and name in ("float32", "float16", "bfloat16", "int64", "int32", "uint8", "bool"):
|
|
116
|
+
return name
|
|
117
|
+
return type(name, (_Obj,), {"__module__": "stub." + module})
|
|
118
|
+
|
|
119
|
+
def persistent_load(self, pid):
|
|
120
|
+
return pid[1] if isinstance(pid[1], str) else "untyped"
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
CHECKPOINT_SKIP = re.compile(r"optim|sched|scaler|rng|history|ema_|swa_", re.I)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def model_tensors(obj):
|
|
127
|
+
if isinstance(obj, dict) and not all(isinstance(v, Tensor) for v in obj.values()):
|
|
128
|
+
parts = {k: OrderedDict(_leaves(v)) for k, v in obj.items()
|
|
129
|
+
if not CHECKPOINT_SKIP.search(str(k)) and not isinstance(v, Tensor)}
|
|
130
|
+
parts = {k: v for k, v in parts.items() if v}
|
|
131
|
+
if parts:
|
|
132
|
+
return max(parts.values(), key=len)
|
|
133
|
+
return OrderedDict(_leaves(obj))
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _torch(p):
|
|
137
|
+
if not zipfile.is_zipfile(p):
|
|
138
|
+
raise ValueError("legacy (non-zip) torch file; re-save with torch>=1.6 to inspect")
|
|
139
|
+
with zipfile.ZipFile(p) as z:
|
|
140
|
+
names = z.namelist()
|
|
141
|
+
obj = _Unpickler(z.open(next(n for n in names if n.endswith("data.pkl")))).load()
|
|
142
|
+
tensors = model_tensors(obj)
|
|
143
|
+
kind = "state_dict" if isinstance(obj, dict) and all(isinstance(v, Tensor) for v in obj.values()) else \
|
|
144
|
+
"checkpoint dict" if isinstance(obj, dict) else type(obj).__name__
|
|
145
|
+
if any("/code/" in n for n in names):
|
|
146
|
+
kind = "TorchScript"
|
|
147
|
+
props = [("Format", f"PyTorch zip · {kind}"), ("Tensors", f"{len(tensors):,}")]
|
|
148
|
+
if isinstance(obj, dict):
|
|
149
|
+
extra = [k for k, v in obj.items() if not isinstance(v, Tensor)][:6]
|
|
150
|
+
if extra:
|
|
151
|
+
props.append(("Keys", ", ".join(map(str, extra))))
|
|
152
|
+
return tensors, props
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _engine(p):
|
|
156
|
+
with open(p, "rb") as f:
|
|
157
|
+
head = f.read(32)
|
|
158
|
+
magic = head[:4].decode("latin-1")
|
|
159
|
+
props = [("Format", "TensorRT engine" if magic == "ftrt" else "TensorRT plan"),
|
|
160
|
+
("Note", "built for one TensorRT/GPU combination; layers need that runtime")]
|
|
161
|
+
return {}, props
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
_SKIP = {"_modules", "_parameters", "_buffers", "state", "args", "params"}
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _leaves(obj, prefix=""):
|
|
168
|
+
if isinstance(obj, Tensor):
|
|
169
|
+
yield prefix.rstrip("."), obj
|
|
170
|
+
elif isinstance(obj, dict):
|
|
171
|
+
for k, v in obj.items():
|
|
172
|
+
yield from _leaves(v, prefix if str(k) in _SKIP else f"{prefix}{k}.")
|
|
173
|
+
elif isinstance(obj, (list, tuple)):
|
|
174
|
+
for i, v in enumerate(obj):
|
|
175
|
+
yield from _leaves(v, f"{prefix}{i}.")
|
|
176
|
+
elif isinstance(obj, _Obj):
|
|
177
|
+
yield from _leaves(getattr(obj, "state", None) or getattr(obj, "args", ()), prefix)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _layers(tensors):
|
|
181
|
+
parts = {k: [s for s in re.split(r"[./:]", k) if s] for k in tensors}
|
|
182
|
+
best = []
|
|
183
|
+
top = Counter()
|
|
184
|
+
for k, t in tensors.items():
|
|
185
|
+
top[parts[k][0] if parts[k] else k] += t.size
|
|
186
|
+
if len(top) > MAX_LAYERS:
|
|
187
|
+
head = top.most_common(MAX_LAYERS - 1)
|
|
188
|
+
return head + [("other", sum(top.values()) - sum(n for _, n in head))]
|
|
189
|
+
for depth in range(1, 8):
|
|
190
|
+
groups = OrderedDict()
|
|
191
|
+
for k, t in tensors.items():
|
|
192
|
+
key = ".".join(parts[k][:depth]) or k
|
|
193
|
+
groups[key] = groups.get(key, 0) + t.size
|
|
194
|
+
if len(groups) > MAX_LAYERS:
|
|
195
|
+
break
|
|
196
|
+
best = list(groups.items())
|
|
197
|
+
if all(len(p) <= depth for p in parts.values()):
|
|
198
|
+
break
|
|
199
|
+
return best
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
READERS = {".onnx": _onnx, ".safetensors": _safetensors, ".msgpack": _msgpack, ".pt": _torch, ".pth": _torch,
|
|
203
|
+
".ckpt": _torch, ".bin": _torch, ".engine": _engine, ".plan": _engine, ".trt": _engine}
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def inspect(path):
|
|
207
|
+
p = Path(path)
|
|
208
|
+
key = (str(p), p.stat().st_mtime)
|
|
209
|
+
if key not in _cache:
|
|
210
|
+
reader = READERS.get(p.suffix.lower())
|
|
211
|
+
try:
|
|
212
|
+
tensors, props = reader(p) if reader else ({}, [("Format", p.suffix[1:].upper() or "file")])
|
|
213
|
+
except Exception as e:
|
|
214
|
+
tensors, props = {}, [("Could not read", f"{type(e).__name__}: {str(e)[:80]}")]
|
|
215
|
+
total = sum(t.size for t in tensors.values())
|
|
216
|
+
stats = [("Parameters", count(total))] if tensors else []
|
|
217
|
+
_cache[key] = dict(stats=stats, props=props, layers=_layers(tensors))
|
|
218
|
+
return _cache[key]
|
laboard/netronview.py
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
from PyQt6.QtCore import QCoreApplication, Qt, QUrl
|
|
5
|
+
from PyQt6.QtGui import QColor
|
|
6
|
+
|
|
7
|
+
from . import theme
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import netron
|
|
11
|
+
from PyQt6 import sip
|
|
12
|
+
from PyQt6.QtWebEngineCore import QWebEngineScript
|
|
13
|
+
from PyQt6.QtWebEngineWidgets import QWebEngineView
|
|
14
|
+
AVAILABLE = True
|
|
15
|
+
except ImportError:
|
|
16
|
+
AVAILABLE = False
|
|
17
|
+
|
|
18
|
+
logging.getLogger("netron").setLevel(logging.WARNING)
|
|
19
|
+
_view, _path, _address = None, None, None
|
|
20
|
+
_win = None
|
|
21
|
+
SIDEBAR_FONT_PX = 14
|
|
22
|
+
SIDEBAR_WIDTH = 0.7
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def prepare():
|
|
26
|
+
if AVAILABLE:
|
|
27
|
+
os.environ.setdefault("QTWEBENGINE_CHROMIUM_FLAGS", "--disable-gpu")
|
|
28
|
+
QCoreApplication.setAttribute(Qt.ApplicationAttribute.AA_ShareOpenGLContexts)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def url():
|
|
32
|
+
return f"http://{_address[0]}:{_address[1]}" if _address else ""
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _new_view(parent=None):
|
|
36
|
+
v = QWebEngineView(parent)
|
|
37
|
+
script = QWebEngineScript()
|
|
38
|
+
script.setName("laboard-sidebar-size")
|
|
39
|
+
script.setInjectionPoint(QWebEngineScript.InjectionPoint.DocumentReady)
|
|
40
|
+
script.setWorldId(QWebEngineScript.ScriptWorldId.ApplicationWorld)
|
|
41
|
+
script.setSourceCode(f"""(() => {{ const s = document.createElement('style');
|
|
42
|
+
s.textContent = '.sidebar {{ zoom: {SIDEBAR_FONT_PX / 11:.4f}; '
|
|
43
|
+
+ 'width: calc(min(calc(100% * 0.6), 42em) * {SIDEBAR_WIDTH}) !important; }}';
|
|
44
|
+
document.head.appendChild(s); }})();""")
|
|
45
|
+
v.page().scripts().insert(script)
|
|
46
|
+
v.loadFinished.connect(lambda ok: _apply_theme(v))
|
|
47
|
+
return v
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _apply_theme(v):
|
|
51
|
+
dark = theme.current == "dark"
|
|
52
|
+
v.page().setBackgroundColor(QColor(theme.T.stage))
|
|
53
|
+
v.page().runJavaScript(f"""(dark => {{
|
|
54
|
+
const seen = window.laboardDark = window.laboardDark || new Set();
|
|
55
|
+
for (const sheet of document.styleSheets) {{
|
|
56
|
+
let rules;
|
|
57
|
+
try {{ rules = sheet.cssRules; }} catch (e) {{ continue; }}
|
|
58
|
+
for (const r of rules)
|
|
59
|
+
if (r.media && (seen.has(r) || r.media.mediaText.includes('prefers-color-scheme: dark'))) {{
|
|
60
|
+
seen.add(r);
|
|
61
|
+
r.media.mediaText = dark ? 'all' : 'not all';
|
|
62
|
+
}}
|
|
63
|
+
}}
|
|
64
|
+
let s = document.getElementById('laboard-stage');
|
|
65
|
+
if (!s && document.head) {{
|
|
66
|
+
s = document.createElement('style');
|
|
67
|
+
s.id = 'laboard-stage';
|
|
68
|
+
document.head.appendChild(s);
|
|
69
|
+
}}
|
|
70
|
+
if (s) s.textContent = '.default, .target, .welcome {{ background-color: {theme.T.stage} !important; }}';
|
|
71
|
+
}})({'true' if dark else 'false'});""")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def attach(win):
|
|
75
|
+
global _view, _win
|
|
76
|
+
_win = win
|
|
77
|
+
if AVAILABLE and _view is None:
|
|
78
|
+
_view = _new_view(win)
|
|
79
|
+
_view.load(QUrl("about:blank"))
|
|
80
|
+
_stow(_view)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _stow(v):
|
|
84
|
+
v.setGeometry(0, 0, 1, 1)
|
|
85
|
+
v.show()
|
|
86
|
+
v.lower()
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def park(win):
|
|
90
|
+
if _view is not None and not sip.isdeleted(_view) and _view.parent() is not win:
|
|
91
|
+
_view.setParent(win)
|
|
92
|
+
_stow(_view)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def view(path):
|
|
96
|
+
global _view, _path, _address
|
|
97
|
+
if _view is None or sip.isdeleted(_view):
|
|
98
|
+
_view, _path = _new_view(_win), None
|
|
99
|
+
if path != _path:
|
|
100
|
+
if _address:
|
|
101
|
+
netron.stop(_address)
|
|
102
|
+
_address = netron.serve(path, address=("localhost", 0), browse=False)
|
|
103
|
+
_path = path
|
|
104
|
+
_view.load(QUrl(url()))
|
|
105
|
+
_apply_theme(_view)
|
|
106
|
+
_view.show()
|
|
107
|
+
return _view
|