cy0307 commited on
Commit
9ed7dde
·
verified ·
1 Parent(s): be9ccf3

Publish Ropedia minimal task baseline weights

Browse files
.gitattributes CHANGED
@@ -34,3 +34,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/task_suite_infographic.png filter=lfs diff=lfs merge=lfs -text
 
 
 
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
  assets/task_suite_infographic.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/task_architectures.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/task_architectures_base.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -63,7 +63,7 @@ Their purpose is to make every input/output contract auditable before scaling to
63
  | `artifacts/**/model.npz` | stores the exact lightweight weights and scalers |
64
  | `artifacts/**/metrics.json` | records the committed metric values |
65
  | `artifacts/**/feature_manifest.json` | maps feature blocks back to source modalities |
66
- | `assets/task_architectures.svg` | shows the shared pipeline and all 12 heads |
67
  | `assets/task_suite_infographic.png` | presents the 12 heads with public-sample modality thumbnails and verified metrics |
68
 
69
  ## Included
@@ -92,7 +92,7 @@ https://huggingface.co/collections/cy0307/ropedia-episode-task-suite
92
 
93
  ## Minimal Architecture
94
 
95
- ![Minimal 12-task architecture](assets/task_architectures.svg)
96
 
97
  ## Metrics Snapshot
98
 
 
63
  | `artifacts/**/model.npz` | stores the exact lightweight weights and scalers |
64
  | `artifacts/**/metrics.json` | records the committed metric values |
65
  | `artifacts/**/feature_manifest.json` | maps feature blocks back to source modalities |
66
+ | `assets/task_architectures.png` | shows the shared pipeline and all 12 heads |
67
  | `assets/task_suite_infographic.png` | presents the 12 heads with public-sample modality thumbnails and verified metrics |
68
 
69
  ## Included
 
92
 
93
  ## Minimal Architecture
94
 
95
+ ![Minimal 12-task architecture](assets/task_architectures.png)
96
 
97
  ## Metrics Snapshot
98
 
assets/task_architectures.png ADDED

Git LFS Details

  • SHA256: 6ca0fc7ba530c7c9ed3501f0b0f5a822a9e1615a4121299ab3866978bfb40cad
  • Pointer size: 131 Bytes
  • Size of remote file: 752 kB
assets/task_architectures_base.png ADDED

Git LFS Details

  • SHA256: 901fb8f50da89d28d1ca716943d2f37c33acbc38372d9f902a50a9bf9e1890d1
  • Pointer size: 132 Bytes
  • Size of remote file: 1.63 MB
notes/episode_task_suite.md CHANGED
@@ -92,7 +92,7 @@ Task-specific architecture details:
92
  Diagram:
93
 
94
  ```text
95
- docs/assets/task_architectures.svg
96
  ```
97
 
98
  ## Current Results
 
92
  Diagram:
93
 
94
  ```text
95
+ docs/assets/task_architectures.png
96
  ```
97
 
98
  ## Current Results
scripts/render_overview_figures.py ADDED
@@ -0,0 +1,766 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Render polished PNG overview figures for the Ropedia project page.
4
+
5
+ The ChatGPT-image assets are used only as text-free visual backgrounds. All
6
+ labels, dimensions, task names, and metrics are read from committed result
7
+ files via scripts/generate_visualizations.py so the final figures stay
8
+ auditable.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import base64
15
+ import html
16
+ import subprocess
17
+ import tempfile
18
+ from pathlib import Path
19
+
20
+ from generate_visualizations import collect_summary, task_architecture_rows
21
+
22
+
23
+ ROOT = Path(__file__).resolve().parents[1]
24
+ ASSETS = ROOT / "docs/assets"
25
+ DEFAULT_PIPELINE_BASE = ASSETS / "pipeline_diagram_base.png"
26
+ DEFAULT_ARCHITECTURE_BASE = ASSETS / "task_architectures_base.png"
27
+ DEFAULT_PIPELINE_OUTPUT = ASSETS / "pipeline_diagram.png"
28
+ DEFAULT_ARCHITECTURE_OUTPUT = ASSETS / "task_architectures.png"
29
+
30
+ PIPELINE_WIDTH = 1800
31
+ PIPELINE_HEIGHT = 1000
32
+ ARCHITECTURE_WIDTH = 1800
33
+ ARCHITECTURE_HEIGHT = 1520
34
+
35
+
36
+ COLORS = {
37
+ "blue": "#1f6c9f",
38
+ "teal": "#197d83",
39
+ "green": "#346538",
40
+ "amber": "#956400",
41
+ "orange": "#b65b04",
42
+ "red": "#9f2f2d",
43
+ "ink": "#1f2421",
44
+ "muted": "#5f625d",
45
+ "line": "#e4ded4",
46
+ }
47
+
48
+
49
+ TASK_GROUPS = [
50
+ ("Label + State", "#197d83", ["timeline_action", "timeline_subtask", "next_action"]),
51
+ (
52
+ "Prediction + Reconstruction",
53
+ "#1f6c9f",
54
+ ["hand_trajectory_forecast", "modality_reconstruction", "contact_prediction"],
55
+ ),
56
+ ("Grounding + Retrieval", "#956400", ["caption_grounding", "cross_modal_retrieval", "object_relevance"]),
57
+ ("Temporal Diagnostics", "#9f2f2d", ["transition_detection", "temporal_order", "misalignment_detection"]),
58
+ ]
59
+
60
+
61
+ def data_uri(path: Path) -> str:
62
+ if not path.exists():
63
+ return ""
64
+ encoded = base64.b64encode(path.read_bytes()).decode("ascii")
65
+ return f"data:image/png;base64,{encoded}"
66
+
67
+
68
+ def esc(value: object) -> str:
69
+ return html.escape(str(value), quote=True)
70
+
71
+
72
+ def build_base_layer(path: Path, opacity: float) -> str:
73
+ uri = data_uri(path)
74
+ if not uri:
75
+ return ""
76
+ return f'<div class="base-layer" style="background-image:url({uri});opacity:{opacity};"></div>'
77
+
78
+
79
+ def stage_card(number: str, title: str, lines: list[str], color: str) -> str:
80
+ detail = "".join(f"<li>{esc(line)}</li>" for line in lines)
81
+ return f"""
82
+ <article class="stage" style="--accent:{color}">
83
+ <div class="stage-number">{esc(number)}</div>
84
+ <h3>{esc(title)}</h3>
85
+ <ul>{detail}</ul>
86
+ </article>
87
+ """
88
+
89
+
90
+ def arrow() -> str:
91
+ return '<div class="flow-arrow" aria-hidden="true">-&gt;</div>'
92
+
93
+
94
+ def build_pipeline_html(summary: dict, base_path: Path) -> str:
95
+ suite = summary["suite"]
96
+ task_count = len(suite["tasks"])
97
+ stage_rows = [
98
+ [
99
+ stage_card(
100
+ "01",
101
+ "Raw public sample",
102
+ ["annotation.hdf5", "6 camera videos", f"{suite['num_frames']:,} aligned frames"],
103
+ COLORS["blue"],
104
+ ),
105
+ arrow(),
106
+ stage_card(
107
+ "02",
108
+ "HOMIE loader",
109
+ ["mocap, hands, IMU", "depth and confidence", "SLAM + calibration"],
110
+ COLORS["teal"],
111
+ ),
112
+ arrow(),
113
+ stage_card(
114
+ "03",
115
+ "Window builder",
116
+ [
117
+ f"{suite['window_frames']}-frame windows",
118
+ f"{suite['stride_frames']}-frame stride",
119
+ f"{suite['num_windows']:,} windows",
120
+ ],
121
+ COLORS["green"],
122
+ ),
123
+ arrow(),
124
+ stage_card(
125
+ "04",
126
+ "Feature vector",
127
+ [f"{suite['feature_dim']:,} dimensions", "17 named blocks", "manifested slice indices"],
128
+ COLORS["orange"],
129
+ ),
130
+ ],
131
+ [
132
+ stage_card(
133
+ "05",
134
+ "Baseline models",
135
+ ["motion-only classifiers", "all-modality classifiers", "stored weights + predictions"],
136
+ COLORS["blue"],
137
+ ),
138
+ arrow(),
139
+ stage_card(
140
+ "06",
141
+ "Episode task suite",
142
+ [f"{task_count} task contracts", "forecast, retrieval, alignment", "chronological evaluation"],
143
+ COLORS["teal"],
144
+ ),
145
+ arrow(),
146
+ stage_card(
147
+ "07",
148
+ "Published artifacts",
149
+ ["metrics.json / csv / npz", "GitHub Pages dashboard", "HF Space + dataset + model card"],
150
+ COLORS["green"],
151
+ ),
152
+ ],
153
+ ]
154
+ rows_html = "".join(f'<section class="flow-row">{"".join(row)}</section>' for row in stage_rows)
155
+ checks = [
156
+ "Audit check: rerunning scripts to /private/tmp reproduced the committed metrics exactly.",
157
+ "Video/depth check: fresh cache read depth plus fisheye_cam0/1/2/3 and stereo_left/right from raw files.",
158
+ "Scope check: this validates one public sample episode, not cross-episode generalization.",
159
+ ]
160
+ checks_html = "".join(f"<li>{esc(line)}</li>" for line in checks)
161
+ base_layer = build_base_layer(base_path, 0.42)
162
+ return f"""<!doctype html>
163
+ <html lang="en">
164
+ <head>
165
+ <meta charset="utf-8">
166
+ <style>
167
+ * {{ box-sizing: border-box; }}
168
+ body {{ margin: 0; background: #fbfaf7; font-family: "Avenir Next", "SF Pro Display", Arial, sans-serif; }}
169
+ .canvas {{
170
+ position: relative;
171
+ width: {PIPELINE_WIDTH}px;
172
+ height: {PIPELINE_HEIGHT}px;
173
+ overflow: hidden;
174
+ color: #1f2421;
175
+ background:
176
+ linear-gradient(90deg, rgba(68,55,38,0.03) 1px, transparent 1px),
177
+ linear-gradient(0deg, rgba(68,55,38,0.024) 1px, transparent 1px),
178
+ #fbfaf7;
179
+ background-size: 58px 58px, 58px 58px, auto;
180
+ }}
181
+ .base-layer {{
182
+ position: absolute;
183
+ inset: 0;
184
+ background-size: cover;
185
+ background-position: center;
186
+ filter: saturate(0.95) contrast(0.98);
187
+ }}
188
+ .wash {{
189
+ position: absolute;
190
+ inset: 0;
191
+ background: linear-gradient(180deg, rgba(251,250,247,0.72), rgba(251,250,247,0.9));
192
+ }}
193
+ .content {{
194
+ position: relative;
195
+ padding: 66px 82px;
196
+ height: 100%;
197
+ }}
198
+ header {{
199
+ display: grid;
200
+ grid-template-columns: 1fr auto;
201
+ gap: 44px;
202
+ align-items: start;
203
+ margin-bottom: 42px;
204
+ }}
205
+ .kicker {{
206
+ font: 700 17px "SF Mono", Menlo, monospace;
207
+ color: #68665f;
208
+ text-transform: uppercase;
209
+ letter-spacing: 0.09em;
210
+ margin-bottom: 14px;
211
+ }}
212
+ h1 {{
213
+ margin: 0;
214
+ font-size: 56px;
215
+ line-height: 0.98;
216
+ letter-spacing: 0;
217
+ }}
218
+ .subtitle {{
219
+ margin: 18px 0 0;
220
+ max-width: 1010px;
221
+ color: #4d524d;
222
+ font-size: 24px;
223
+ line-height: 1.42;
224
+ font-weight: 520;
225
+ }}
226
+ .metrics {{
227
+ display: grid;
228
+ grid-template-columns: repeat(2, 150px);
229
+ gap: 12px;
230
+ margin-top: 2px;
231
+ }}
232
+ .metric {{
233
+ background: rgba(255,254,253,0.86);
234
+ border: 1px solid #e4ded4;
235
+ border-radius: 8px;
236
+ padding: 13px 15px 12px;
237
+ box-shadow: 0 16px 40px rgba(68,55,38,0.06);
238
+ }}
239
+ .metric strong {{
240
+ display: block;
241
+ font: 850 24px "SF Mono", Menlo, monospace;
242
+ color: #1f2421;
243
+ line-height: 1;
244
+ font-variant-numeric: tabular-nums;
245
+ }}
246
+ .metric span {{
247
+ display: block;
248
+ margin-top: 7px;
249
+ color: #696d67;
250
+ font-size: 14px;
251
+ font-weight: 650;
252
+ }}
253
+ .flow-row {{
254
+ display: flex;
255
+ align-items: center;
256
+ gap: 18px;
257
+ margin-top: 30px;
258
+ }}
259
+ .flow-row:nth-of-type(2) {{
260
+ width: 78%;
261
+ margin-left: auto;
262
+ margin-right: auto;
263
+ margin-top: 38px;
264
+ }}
265
+ .stage {{
266
+ min-width: 0;
267
+ flex: 1 1 0;
268
+ height: 182px;
269
+ position: relative;
270
+ background: rgba(255,254,253,0.88);
271
+ border: 1px solid rgba(228,222,212,0.96);
272
+ border-radius: 8px;
273
+ padding: 24px 24px 22px 30px;
274
+ box-shadow: 0 24px 62px rgba(68,55,38,0.09);
275
+ backdrop-filter: blur(12px);
276
+ }}
277
+ .stage::before {{
278
+ content: "";
279
+ position: absolute;
280
+ inset: 0 auto 0 0;
281
+ width: 8px;
282
+ border-radius: 8px 0 0 8px;
283
+ background: var(--accent);
284
+ }}
285
+ .stage-number {{
286
+ color: var(--accent);
287
+ font: 850 16px "SF Mono", Menlo, monospace;
288
+ letter-spacing: 0.04em;
289
+ margin-bottom: 10px;
290
+ }}
291
+ .stage h3 {{
292
+ margin: 0 0 13px;
293
+ font-size: 24px;
294
+ line-height: 1.08;
295
+ letter-spacing: 0;
296
+ }}
297
+ .stage ul {{
298
+ margin: 0;
299
+ padding: 0;
300
+ list-style: none;
301
+ color: #39413d;
302
+ font-size: 17px;
303
+ line-height: 1.48;
304
+ font-weight: 560;
305
+ }}
306
+ .flow-arrow {{
307
+ width: 54px;
308
+ flex: 0 0 54px;
309
+ height: 54px;
310
+ display: grid;
311
+ place-items: center;
312
+ border-radius: 999px;
313
+ border: 1px solid #d7d0c4;
314
+ background: rgba(255,254,253,0.78);
315
+ color: #7c807a;
316
+ font: 850 22px "SF Mono", Menlo, monospace;
317
+ box-shadow: 0 14px 34px rgba(68,55,38,0.06);
318
+ }}
319
+ .audit {{
320
+ position: absolute;
321
+ left: 82px;
322
+ right: 82px;
323
+ bottom: 62px;
324
+ display: grid;
325
+ grid-template-columns: 190px 1fr;
326
+ gap: 26px;
327
+ align-items: center;
328
+ background: rgba(255,254,253,0.88);
329
+ border: 1px solid #e4ded4;
330
+ border-radius: 8px;
331
+ padding: 24px 28px;
332
+ box-shadow: 0 22px 52px rgba(68,55,38,0.08);
333
+ }}
334
+ .audit strong {{
335
+ color: #1f2421;
336
+ font-size: 23px;
337
+ line-height: 1.1;
338
+ }}
339
+ .audit ul {{
340
+ margin: 0;
341
+ padding: 0;
342
+ list-style: none;
343
+ color: #40463f;
344
+ font-size: 17px;
345
+ line-height: 1.55;
346
+ font-weight: 560;
347
+ }}
348
+ </style>
349
+ </head>
350
+ <body>
351
+ <main class="canvas">
352
+ {base_layer}
353
+ <div class="wash"></div>
354
+ <div class="content">
355
+ <header>
356
+ <div>
357
+ <div class="kicker">verified single-episode pipeline</div>
358
+ <h1>From Ropedia episode to reproducible artifacts</h1>
359
+ <p class="subtitle">The figure follows the actual code path: raw multimodal sample data, aligned windows, feature vectors, minimal heads, and published metrics.</p>
360
+ </div>
361
+ <div class="metrics">
362
+ <div class="metric"><strong>{suite['num_frames']:,}</strong><span>frames</span></div>
363
+ <div class="metric"><strong>{suite['num_windows']:,}</strong><span>windows</span></div>
364
+ <div class="metric"><strong>{suite['feature_dim']:,}</strong><span>features</span></div>
365
+ <div class="metric"><strong>{task_count}</strong><span>tasks</span></div>
366
+ </div>
367
+ </header>
368
+ {rows_html}
369
+ <section class="audit">
370
+ <strong>Reproducibility checks</strong>
371
+ <ul>{checks_html}</ul>
372
+ </section>
373
+ </div>
374
+ </main>
375
+ </body>
376
+ </html>
377
+ """
378
+
379
+
380
+ def family_label(family: str) -> str:
381
+ return {
382
+ "softmax": "linear softmax",
383
+ "ridge": "ridge regression",
384
+ "ridge+rank": "ridge + cosine rank",
385
+ "multilabel": "multi-label logistic",
386
+ }.get(family, family)
387
+
388
+
389
+ def build_task_card(row: dict, color: str) -> str:
390
+ return f"""
391
+ <article class="task-card" style="--accent:{color}">
392
+ <div class="chip">{esc(family_label(row['family']))}</div>
393
+ <h3>{esc(row['task'])}</h3>
394
+ <dl>
395
+ <dt>Input</dt><dd>{esc(row['input'])}</dd>
396
+ <dt>Head</dt><dd>{esc(row['head'])}</dd>
397
+ <dt>Output</dt><dd>{esc(row['output'])}</dd>
398
+ </dl>
399
+ <div class="metric-line"><span>Metric</span><strong>{esc(row['metric'])}</strong></div>
400
+ </article>
401
+ """
402
+
403
+
404
+ def build_architecture_html(summary: dict, base_path: Path) -> str:
405
+ suite = summary["suite"]
406
+ rows_by_task = {row["task"]: row for row in task_architecture_rows(summary)}
407
+ group_html = []
408
+ for title, color, task_names in TASK_GROUPS:
409
+ cards = "".join(build_task_card(rows_by_task[name], color) for name in task_names)
410
+ group_html.append(
411
+ f"""
412
+ <section class="task-group" style="--accent:{color}">
413
+ <div class="group-head">
414
+ <span></span>
415
+ <h2>{esc(title)}</h2>
416
+ </div>
417
+ <div class="group-cards">{cards}</div>
418
+ </section>
419
+ """
420
+ )
421
+
422
+ family_cards = [
423
+ ("Linear softmax", "Class-weighted CE + L2 for action, subtask, next-action, transition, contact, order, and alignment classifiers.", COLORS["blue"]),
424
+ ("Ridge regression", "Closed-form dual ridge for hand trajectory forecasting and modality reconstruction.", COLORS["green"]),
425
+ ("Ridge + cosine rank", "Project sensor features into text or visual space, then rank candidate windows by cosine similarity.", COLORS["teal"]),
426
+ ("Multi-label logistic", "One-vs-rest sigmoid heads over the object vocabulary with top-1 fallback.", COLORS["orange"]),
427
+ ]
428
+ families = "".join(
429
+ f"""
430
+ <article class="family" style="--accent:{color}">
431
+ <h3>{esc(title)}</h3>
432
+ <p>{esc(desc)}</p>
433
+ </article>
434
+ """
435
+ for title, desc, color in family_cards
436
+ )
437
+ base_layer = build_base_layer(base_path, 0.36)
438
+ return f"""<!doctype html>
439
+ <html lang="en">
440
+ <head>
441
+ <meta charset="utf-8">
442
+ <style>
443
+ * {{ box-sizing: border-box; }}
444
+ body {{ margin: 0; background: #fbfaf7; font-family: "Avenir Next", "SF Pro Display", Arial, sans-serif; }}
445
+ .canvas {{
446
+ position: relative;
447
+ width: {ARCHITECTURE_WIDTH}px;
448
+ height: {ARCHITECTURE_HEIGHT}px;
449
+ overflow: hidden;
450
+ color: #1f2421;
451
+ background:
452
+ linear-gradient(90deg, rgba(68,55,38,0.03) 1px, transparent 1px),
453
+ linear-gradient(0deg, rgba(68,55,38,0.024) 1px, transparent 1px),
454
+ #fbfaf7;
455
+ background-size: 58px 58px, 58px 58px, auto;
456
+ }}
457
+ .base-layer {{
458
+ position: absolute;
459
+ inset: 0;
460
+ background-size: cover;
461
+ background-position: center;
462
+ filter: saturate(0.95) contrast(0.98);
463
+ }}
464
+ .wash {{
465
+ position: absolute;
466
+ inset: 0;
467
+ background: linear-gradient(180deg, rgba(251,250,247,0.72), rgba(251,250,247,0.92));
468
+ }}
469
+ .content {{
470
+ position: relative;
471
+ height: 100%;
472
+ padding: 58px 74px 64px;
473
+ }}
474
+ header {{
475
+ display: grid;
476
+ grid-template-columns: 1fr auto;
477
+ gap: 42px;
478
+ align-items: start;
479
+ margin-bottom: 28px;
480
+ }}
481
+ .kicker {{
482
+ font: 700 16px "SF Mono", Menlo, monospace;
483
+ color: #68665f;
484
+ text-transform: uppercase;
485
+ letter-spacing: 0.09em;
486
+ margin-bottom: 13px;
487
+ }}
488
+ h1 {{
489
+ margin: 0;
490
+ font-size: 52px;
491
+ line-height: 1;
492
+ letter-spacing: 0;
493
+ }}
494
+ .subtitle {{
495
+ margin: 15px 0 0;
496
+ max-width: 1060px;
497
+ color: #4d524d;
498
+ font-size: 22px;
499
+ line-height: 1.42;
500
+ font-weight: 520;
501
+ }}
502
+ .summary-pill {{
503
+ display: grid;
504
+ place-items: center;
505
+ min-width: 188px;
506
+ min-height: 112px;
507
+ border: 1px solid #e4ded4;
508
+ border-radius: 8px;
509
+ background: rgba(255,254,253,0.86);
510
+ box-shadow: 0 18px 44px rgba(68,55,38,0.07);
511
+ text-align: center;
512
+ }}
513
+ .summary-pill strong {{
514
+ font: 850 36px "SF Mono", Menlo, monospace;
515
+ line-height: 1;
516
+ }}
517
+ .summary-pill span {{
518
+ display: block;
519
+ margin-top: 8px;
520
+ color: #6c6d68;
521
+ font-size: 15px;
522
+ font-weight: 700;
523
+ }}
524
+ .shared {{
525
+ display: grid;
526
+ grid-template-columns: repeat(4, 1fr);
527
+ gap: 18px;
528
+ margin-bottom: 24px;
529
+ }}
530
+ .shared article {{
531
+ min-height: 110px;
532
+ border: 1px solid #e4ded4;
533
+ border-radius: 8px;
534
+ background: rgba(255,254,253,0.88);
535
+ padding: 20px 22px;
536
+ box-shadow: 0 18px 44px rgba(68,55,38,0.06);
537
+ }}
538
+ .shared h2 {{
539
+ margin: 0 0 9px;
540
+ font-size: 22px;
541
+ line-height: 1.08;
542
+ }}
543
+ .shared p {{
544
+ margin: 0;
545
+ color: #4c534f;
546
+ font-size: 16px;
547
+ line-height: 1.38;
548
+ font-weight: 560;
549
+ }}
550
+ .families {{
551
+ display: grid;
552
+ grid-template-columns: repeat(4, 1fr);
553
+ gap: 18px;
554
+ margin-bottom: 28px;
555
+ }}
556
+ .family {{
557
+ min-height: 124px;
558
+ border: 1px solid #e4ded4;
559
+ border-radius: 8px;
560
+ background: rgba(255,254,253,0.82);
561
+ padding: 20px 20px 18px;
562
+ box-shadow: 0 16px 40px rgba(68,55,38,0.055);
563
+ }}
564
+ .family h3 {{
565
+ margin: 0 0 10px;
566
+ color: var(--accent);
567
+ font-size: 21px;
568
+ line-height: 1.08;
569
+ }}
570
+ .family p {{
571
+ margin: 0;
572
+ color: #4d534f;
573
+ font-size: 15px;
574
+ line-height: 1.42;
575
+ font-weight: 560;
576
+ }}
577
+ .task-groups {{
578
+ display: grid;
579
+ grid-template-columns: repeat(4, 1fr);
580
+ gap: 20px;
581
+ }}
582
+ .task-group {{
583
+ border: 1px solid rgba(228,222,212,0.96);
584
+ border-radius: 8px;
585
+ background: rgba(255,254,253,0.74);
586
+ padding: 18px;
587
+ box-shadow: 0 22px 54px rgba(68,55,38,0.07);
588
+ backdrop-filter: blur(10px);
589
+ }}
590
+ .group-head {{
591
+ display: flex;
592
+ align-items: center;
593
+ gap: 11px;
594
+ margin-bottom: 14px;
595
+ }}
596
+ .group-head span {{
597
+ width: 12px;
598
+ height: 34px;
599
+ border-radius: 999px;
600
+ background: var(--accent);
601
+ }}
602
+ .group-head h2 {{
603
+ margin: 0;
604
+ font-size: 24px;
605
+ line-height: 1.08;
606
+ color: var(--accent);
607
+ }}
608
+ .group-cards {{
609
+ display: grid;
610
+ gap: 14px;
611
+ }}
612
+ .task-card {{
613
+ min-height: 244px;
614
+ position: relative;
615
+ border: 1px solid color-mix(in srgb, var(--accent), #ffffff 68%);
616
+ border-radius: 8px;
617
+ background: rgba(255,254,253,0.92);
618
+ padding: 17px 18px 16px;
619
+ overflow: hidden;
620
+ }}
621
+ .task-card::before {{
622
+ content: "";
623
+ position: absolute;
624
+ inset: 0 auto 0 0;
625
+ width: 6px;
626
+ background: var(--accent);
627
+ opacity: 0.92;
628
+ }}
629
+ .chip {{
630
+ display: inline-flex;
631
+ border: 1px solid color-mix(in srgb, var(--accent), #ffffff 35%);
632
+ border-radius: 6px;
633
+ padding: 4px 8px;
634
+ color: var(--accent);
635
+ font: 850 11px "SF Mono", Menlo, monospace;
636
+ text-transform: uppercase;
637
+ letter-spacing: 0.03em;
638
+ background: rgba(255,254,253,0.72);
639
+ }}
640
+ .task-card h3 {{
641
+ margin: 13px 0 12px;
642
+ color: #161a17;
643
+ font-size: 21px;
644
+ line-height: 1.08;
645
+ overflow-wrap: anywhere;
646
+ }}
647
+ dl {{
648
+ display: grid;
649
+ grid-template-columns: 54px 1fr;
650
+ gap: 5px 9px;
651
+ margin: 0;
652
+ color: #3d453f;
653
+ font-size: 13px;
654
+ line-height: 1.32;
655
+ font-weight: 560;
656
+ }}
657
+ dt {{
658
+ color: var(--accent);
659
+ font: 850 10px "SF Mono", Menlo, monospace;
660
+ text-transform: uppercase;
661
+ letter-spacing: 0.04em;
662
+ }}
663
+ dd {{ margin: 0; }}
664
+ .metric-line {{
665
+ display: flex;
666
+ justify-content: space-between;
667
+ gap: 12px;
668
+ align-items: center;
669
+ margin-top: 12px;
670
+ border-top: 1px solid #eee9e1;
671
+ padding-top: 12px;
672
+ font-size: 13px;
673
+ font-weight: 700;
674
+ }}
675
+ .metric-line span {{
676
+ color: #6c6d68;
677
+ font: 850 11px "SF Mono", Menlo, monospace;
678
+ text-transform: uppercase;
679
+ }}
680
+ .metric-line strong {{
681
+ color: var(--accent);
682
+ font: 850 15px "SF Mono", Menlo, monospace;
683
+ text-align: right;
684
+ font-variant-numeric: tabular-nums;
685
+ }}
686
+ </style>
687
+ </head>
688
+ <body>
689
+ <main class="canvas">
690
+ {base_layer}
691
+ <div class="wash"></div>
692
+ <div class="content">
693
+ <header>
694
+ <div>
695
+ <div class="kicker">minimal verified model architectures</div>
696
+ <h1>12 Ropedia episode tasks, four reusable heads</h1>
697
+ <p class="subtitle">Each task uses the same aligned episode-window contract, then swaps only the minimal output head needed for labels, forecasting, grounding, reconstruction, or temporal diagnostics.</p>
698
+ </div>
699
+ <div class="summary-pill"><strong>{len(suite['tasks'])}</strong><span>end-to-end tasks</span></div>
700
+ </header>
701
+ <section class="shared">
702
+ <article><h2>Shared windows</h2><p>{suite['num_frames']:,} frames to {suite['num_windows']:,} windows, {suite['window_frames']}-frame context, {suite['stride_frames']}-frame stride.</p></article>
703
+ <article><h2>Feature vector</h2><p>X_all is {suite['feature_dim']:,} dimensions with 17 named modality blocks and train-fit standardization.</p></article>
704
+ <article><h2>Reusable heads</h2><p>Softmax, ridge, ridge ranking, and multi-label logistic heads cover the whole suite.</p></article>
705
+ <article><h2>Artifacts</h2><p>Metrics, predictions, models, manifests, and the source summary report are committed.</p></article>
706
+ </section>
707
+ <section class="families">{families}</section>
708
+ <section class="task-groups">{"".join(group_html)}</section>
709
+ </div>
710
+ </main>
711
+ </body>
712
+ </html>
713
+ """
714
+
715
+
716
+ def render_html(html_text: str, output_path: Path, width: int, height: int, keep_html: Path | None = None) -> None:
717
+ output_path.parent.mkdir(parents=True, exist_ok=True)
718
+ if keep_html is None:
719
+ with tempfile.NamedTemporaryFile("w", suffix=".html", encoding="utf-8", delete=False) as handle:
720
+ handle.write(html_text)
721
+ html_path = Path(handle.name)
722
+ else:
723
+ html_path = keep_html
724
+ html_path.parent.mkdir(parents=True, exist_ok=True)
725
+ html_path.write_text(html_text, encoding="utf-8")
726
+ subprocess.run(
727
+ [
728
+ "npx",
729
+ "--yes",
730
+ "playwright",
731
+ "screenshot",
732
+ "--full-page",
733
+ f"--viewport-size={width},{height}",
734
+ html_path.resolve().as_uri(),
735
+ str(output_path),
736
+ ],
737
+ check=True,
738
+ )
739
+ print(f"Wrote image: {output_path}")
740
+ print(f"Wrote render HTML: {html_path}")
741
+
742
+
743
+ def main() -> int:
744
+ parser = argparse.ArgumentParser()
745
+ parser.add_argument("--pipeline-base", type=Path, default=DEFAULT_PIPELINE_BASE)
746
+ parser.add_argument("--architecture-base", type=Path, default=DEFAULT_ARCHITECTURE_BASE)
747
+ parser.add_argument("--pipeline-output", type=Path, default=DEFAULT_PIPELINE_OUTPUT)
748
+ parser.add_argument("--architecture-output", type=Path, default=DEFAULT_ARCHITECTURE_OUTPUT)
749
+ parser.add_argument("--html-dir", type=Path, help="Optional directory for the intermediate render HTML files.")
750
+ parser.add_argument("--only", choices=["pipeline", "architecture", "both"], default="both")
751
+ args = parser.parse_args()
752
+
753
+ summary = collect_summary()
754
+ if args.only in {"pipeline", "both"}:
755
+ pipeline_html = build_pipeline_html(summary, args.pipeline_base)
756
+ html_path = args.html_dir / "pipeline_diagram.html" if args.html_dir else None
757
+ render_html(pipeline_html, args.pipeline_output, PIPELINE_WIDTH, PIPELINE_HEIGHT, html_path)
758
+ if args.only in {"architecture", "both"}:
759
+ architecture_html = build_architecture_html(summary, args.architecture_base)
760
+ html_path = args.html_dir / "task_architectures.html" if args.html_dir else None
761
+ render_html(architecture_html, args.architecture_output, ARCHITECTURE_WIDTH, ARCHITECTURE_HEIGHT, html_path)
762
+ return 0
763
+
764
+
765
+ if __name__ == "__main__":
766
+ raise SystemExit(main())