@dxo/studio 0.0.9 → 0.0.11

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.
package/README.md CHANGED
@@ -1,7 +1,25 @@
1
1
  # @dxo/studio
2
2
 
3
- **Placeholder developer preview, API unstable.**
3
+ DXO training workbench (developer preview). **WebUI is VMZ** — not a separate Vue app.
4
4
 
5
- Future VMZ workbench for runs, system metrics, artifacts, model graphs, profiler, and UI playground. This package currently only wires protocol package versions; it does **not** listen on a port yet.
5
+ ## Two entry points
6
6
 
7
- Default future bind: loopback only. Remote deploy requires explicit auth (see Living design).
7
+ | Entry | What it does |
8
+ |----------------------|------------------------------------------------------------|
9
+ | **`dxo studio`** | VMZ **watch serve** + Rust inspect API → **browser** |
10
+ | **`dxo-studio.exe`** | Tauri desktop GUI (embedded VMZ static build + native API) |
11
+
12
+ | Layer | Path |
13
+ |-------------|--------------------------------------------------------------|
14
+ | Rust API | `projects/compilers/dxo-studio` |
15
+ | Rust GUI | `projects/compilers/dxo-studio-tauri` → **`dxo-studio.exe`** |
16
+ | TS launcher | `launcher/` → `dist/` |
17
+ | VMZ WebUI | package root (`src/*.vmz`, `vmz.config.ts`) |
18
+
19
+ ```bash
20
+ pnpm exec dxo studio
21
+ pnpm --filter @dxo/studio dev # VMZ only
22
+ pnpm --filter @dxo/studio build:ui # static profile for Tauri embed
23
+
24
+ cargo build -p dxo-studio-tauri --release # → target/release/dxo-studio.exe
25
+ ```
@@ -0,0 +1,208 @@
1
+ .studio {
2
+ font-family: ui-sans-serif, system-ui, sans-serif;
3
+ max-width: 56rem;
4
+ margin: 0 auto;
5
+ padding: 1.5rem;
6
+ color: #122;
7
+ background: #f7f8f6;
8
+ min-height: 100vh;
9
+ }
10
+
11
+ .studio__header {
12
+ display: flex;
13
+ justify-content: space-between;
14
+ align-items: baseline;
15
+ margin-bottom: 1.5rem;
16
+ border-bottom: 1px solid #ccd;
17
+ padding-bottom: 0.75rem;
18
+ }
19
+
20
+ .studio__brand {
21
+ color: inherit;
22
+ text-decoration: none;
23
+ font-size: 1.25rem;
24
+ }
25
+
26
+ .studio__nav {
27
+ display: flex;
28
+ gap: 1rem;
29
+ }
30
+
31
+ .studio__nav a {
32
+ color: #345;
33
+ text-decoration: none;
34
+ font-size: 0.95rem;
35
+ }
36
+
37
+ .studio__nav a:hover {
38
+ text-decoration: underline;
39
+ }
40
+
41
+ .studio__btn {
42
+ margin-top: 0.75rem;
43
+ margin-right: 0.5rem;
44
+ padding: 0.4rem 0.9rem;
45
+ border: 1px solid #9ab;
46
+ border-radius: 0.35rem;
47
+ background: #dfe8e0;
48
+ cursor: pointer;
49
+ font-size: 0.9rem;
50
+ }
51
+
52
+ .studio__btn:disabled {
53
+ opacity: 0.5;
54
+ cursor: not-allowed;
55
+ }
56
+
57
+ .studio__btn--ghost {
58
+ background: transparent;
59
+ }
60
+
61
+ .studio__pick-list label {
62
+ display: flex;
63
+ gap: 0.5rem;
64
+ align-items: center;
65
+ cursor: pointer;
66
+ }
67
+
68
+ .studio__matrix td,
69
+ .studio__matrix th {
70
+ text-align: center;
71
+ min-width: 2.5rem;
72
+ }
73
+
74
+ .studio__cell--warm {
75
+ background: #e8f0dc;
76
+ }
77
+
78
+ .studio__cell--hot {
79
+ background: #c5ddb0;
80
+ font-weight: 600;
81
+ }
82
+
83
+ .studio__samples {
84
+ list-style: none;
85
+ padding: 0;
86
+ display: grid;
87
+ grid-template-columns: repeat(auto-fill, minmax(8rem, 1fr));
88
+ gap: 0.75rem;
89
+ }
90
+
91
+ .studio__samples li {
92
+ border: 1px solid #dde;
93
+ padding: 0.35rem;
94
+ display: flex;
95
+ flex-direction: column;
96
+ gap: 0.35rem;
97
+ }
98
+
99
+ .studio__samples img {
100
+ width: 100%;
101
+ height: auto;
102
+ display: block;
103
+ background: #eef1ee;
104
+ }
105
+
106
+ .studio__upload .studio__file {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: 0.35rem;
110
+ margin: 1rem 0;
111
+ font-size: 0.9rem;
112
+ }
113
+
114
+ .studio__preview img {
115
+ max-width: 16rem;
116
+ margin-top: 0.75rem;
117
+ border: 1px solid #ccd;
118
+ }
119
+
120
+ .studio__compare {
121
+ margin-top: 1.5rem;
122
+ }
123
+
124
+ .studio__panel h1 {
125
+ font-size: 1.5rem;
126
+ margin: 0 0 0.5rem;
127
+ }
128
+
129
+ .studio__panel h2 {
130
+ font-size: 1.1rem;
131
+ margin: 1.5rem 0 0.5rem;
132
+ }
133
+
134
+ .studio__hint,
135
+ .studio__status,
136
+ .studio__meta {
137
+ color: #456;
138
+ font-size: 0.9rem;
139
+ }
140
+
141
+ .studio__list {
142
+ list-style: none;
143
+ padding: 0;
144
+ margin: 1rem 0;
145
+ }
146
+
147
+ .studio__list li {
148
+ padding: 0.5rem 0;
149
+ border-bottom: 1px solid #dde;
150
+ display: flex;
151
+ flex-wrap: wrap;
152
+ gap: 0.75rem;
153
+ align-items: baseline;
154
+ }
155
+
156
+ .studio__list--compact li {
157
+ font-size: 0.85rem;
158
+ font-family: ui-monospace, monospace;
159
+ }
160
+
161
+ .studio__badge {
162
+ font-size: 0.75rem;
163
+ padding: 0.1rem 0.4rem;
164
+ border-radius: 0.25rem;
165
+ background: #dde8e0;
166
+ }
167
+
168
+ .studio__table {
169
+ width: 100%;
170
+ border-collapse: collapse;
171
+ font-size: 0.9rem;
172
+ }
173
+
174
+ .studio__table th,
175
+ .studio__table td {
176
+ text-align: left;
177
+ padding: 0.35rem 0.5rem;
178
+ border-bottom: 1px solid #dde;
179
+ }
180
+
181
+ .studio__pre {
182
+ background: #eef1ee;
183
+ padding: 0.75rem;
184
+ overflow: auto;
185
+ font-size: 0.8rem;
186
+ max-height: 24rem;
187
+ }
188
+
189
+ .studio__tabs {
190
+ display: flex;
191
+ gap: 0.35rem;
192
+ margin: 0.5rem 0 0.75rem;
193
+ }
194
+
195
+ .studio__tab {
196
+ border: 1px solid #ccd;
197
+ background: #f7f8f7;
198
+ color: #334;
199
+ padding: 0.35rem 0.7rem;
200
+ cursor: pointer;
201
+ font: inherit;
202
+ }
203
+
204
+ .studio__tab--active {
205
+ background: #2a4;
206
+ color: #fff;
207
+ border-color: #2a4;
208
+ }
@@ -1,17 +1,15 @@
1
- import { type Server } from 'node:http';
2
- export type InspectApiServerOptions = {
3
- host?: string;
4
- port?: number;
5
- runsRoot?: string;
6
- };
1
+ import { type NativeInspectApiServerOptions } from '@dxo/core';
2
+ export type InspectApiServerOptions = NativeInspectApiServerOptions;
7
3
  export type InspectApiServer = {
8
4
  host: string;
9
5
  port: number;
10
6
  url: string;
11
7
  runsRoot: string;
12
- server: Server;
13
8
  close: () => Promise<void>;
14
9
  };
15
- /** Loopback HTTP API over the append-only inspect run store. */
10
+ /**
11
+ * Loopback inspect HTTP serve — thin TS wrapper over napi `create_inspect_api_server`.
12
+ * Do not reimplement the HTTP server or run-store reader in TypeScript.
13
+ */
16
14
  export declare function createInspectApiServer(options?: InspectApiServerOptions): Promise<InspectApiServer>;
17
15
  //# sourceMappingURL=api-server.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../src/api-server.ts"],"names":[],"mappings":"AACA,OAAO,EAAgB,KAAK,MAAM,EAAE,MAAM,WAAW,CAAC;AAKtD,MAAM,MAAM,uBAAuB,GAAG;IAClC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAmIF,gEAAgE;AAChE,wBAAsB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAoC7G"}
1
+ {"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../launcher/api-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAA0D,KAAK,6BAA6B,EAAE,MAAM,WAAW,CAAC;AAEvH,MAAM,MAAM,uBAAuB,GAAG,6BAA6B,CAAC;AAEpE,MAAM,MAAM,gBAAgB,GAAG;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAEF;;;GAGG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,GAAE,uBAA4B,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAe7G"}
@@ -1,147 +1,22 @@
1
- import { readFile } from 'node:fs/promises';
2
- import { createServer } from 'node:http';
3
- import path from 'node:path';
4
- import { defaultRunsRoot, listRuns, readEvents, readRunMeta } from '@dxo/inspect';
5
- function json(res, status, body) {
6
- const text = JSON.stringify(body);
7
- res.writeHead(status, {
8
- 'content-type': 'application/json; charset=utf-8',
9
- 'content-length': Buffer.byteLength(text),
10
- 'access-control-allow-origin': '*',
11
- });
12
- res.end(text);
13
- }
14
- function notFound(res) {
15
- json(res, 404, { error: 'not_found' });
16
- }
17
- function parsePath(url) {
18
- const u = new URL(url, 'http://127.0.0.1');
19
- return { pathname: u.pathname, searchParams: u.searchParams };
20
- }
21
- function metricsFromEvents(events) {
22
- const out = [];
23
- for (const event of events) {
24
- if (event.type === 'metric/scalar')
25
- out.push(event.metric);
26
- }
27
- return out;
28
- }
29
- function artifactsFromEvents(events) {
30
- const out = [];
31
- for (const event of events) {
32
- if (event.type === 'artifact/ref')
33
- out.push(event.artifact);
34
- }
35
- return out;
36
- }
37
- async function handleRequest(req, res, runsRoot) {
38
- if (req.method !== 'GET') {
39
- json(res, 405, { error: 'method_not_allowed' });
40
- return;
41
- }
42
- const { pathname } = parsePath(req.url ?? '/');
43
- if (pathname === '/api/health') {
44
- json(res, 200, { ok: true, service: 'dxo-studio-api' });
45
- return;
46
- }
47
- if (pathname === '/api/runs') {
48
- const runs = await listRuns(runsRoot);
49
- json(res, 200, { runs });
50
- return;
51
- }
52
- const runMatch = /^\/api\/runs\/([^/]+)(\/.*)?$/.exec(pathname);
53
- if (!runMatch) {
54
- notFound(res);
55
- return;
56
- }
57
- const runId = decodeURIComponent(runMatch[1]);
58
- const sub = runMatch[2] ?? '';
59
- if (sub === '' || sub === '/meta') {
60
- const meta = await readRunMeta(runsRoot, runId);
61
- if (!meta) {
62
- notFound(res);
63
- return;
64
- }
65
- json(res, 200, { meta });
66
- return;
67
- }
68
- if (sub === '/events') {
69
- const events = await readEvents(runsRoot, runId);
70
- json(res, 200, { runId, events });
71
- return;
72
- }
73
- if (sub === '/metrics') {
74
- const events = await readEvents(runsRoot, runId);
75
- json(res, 200, { runId, metrics: metricsFromEvents(events) });
76
- return;
77
- }
78
- if (sub === '/artifacts') {
79
- const events = await readEvents(runsRoot, runId);
80
- json(res, 200, { runId, artifacts: artifactsFromEvents(events) });
81
- return;
82
- }
83
- if (sub === '/checkpoints') {
84
- const events = await readEvents(runsRoot, runId);
85
- const checkpoints = artifactsFromEvents(events).filter((a) => a.kind === 'checkpoint');
86
- json(res, 200, { runId, checkpoints });
87
- return;
88
- }
89
- if (sub === '/model-graph') {
90
- const abs = path.join(runsRoot, runId, 'artifacts', 'model-graph.json');
91
- try {
92
- const body = await readFile(abs, 'utf8');
93
- json(res, 200, { runId, graph: JSON.parse(body) });
94
- }
95
- catch {
96
- notFound(res);
97
- }
98
- return;
99
- }
100
- const artifactMatch = /^\/artifacts\/(.+)$/.exec(sub);
101
- if (artifactMatch) {
102
- const rel = decodeURIComponent(artifactMatch[1]);
103
- const abs = path.join(runsRoot, runId, rel);
104
- try {
105
- const body = await readFile(abs, 'utf8');
106
- json(res, 200, { runId, path: rel, body });
107
- }
108
- catch {
109
- notFound(res);
110
- }
111
- return;
112
- }
113
- notFound(res);
114
- }
115
- /** Loopback HTTP API over the append-only inspect run store. */
1
+ import { createInspectApiServer as createNativeInspectApiServer } from '@dxo/core';
2
+ /**
3
+ * Loopback inspect HTTP serve — thin TS wrapper over napi `create_inspect_api_server`.
4
+ * Do not reimplement the HTTP server or run-store reader in TypeScript.
5
+ */
116
6
  export async function createInspectApiServer(options = {}) {
117
- const host = options.host ?? '127.0.0.1';
118
- const port = options.port ?? 0;
119
- const runsRoot = options.runsRoot ?? defaultRunsRoot();
120
- const server = createServer((req, res) => {
121
- void handleRequest(req, res, runsRoot).catch((err) => {
122
- const message = err instanceof Error ? err.message : String(err);
123
- json(res, 500, { error: 'internal_error', message });
124
- });
125
- });
126
- await new Promise((resolve, reject) => {
127
- server.once('error', reject);
128
- server.listen(port, host, () => resolve());
7
+ const handle = createNativeInspectApiServer({
8
+ host: options.host,
9
+ port: options.port,
10
+ runsRoot: options.runsRoot,
129
11
  });
130
- const addr = server.address();
131
- if (!addr || typeof addr === 'string') {
132
- throw new Error('failed to bind inspect API server');
133
- }
134
- const boundPort = addr.port;
135
- const url = `http://${host}:${boundPort}`;
136
12
  return {
137
- host,
138
- port: boundPort,
139
- url,
140
- runsRoot,
141
- server,
142
- close: () => new Promise((resolve, reject) => {
143
- server.close((err) => (err ? reject(err) : resolve()));
144
- }),
13
+ host: handle.host,
14
+ port: handle.port,
15
+ url: handle.url,
16
+ runsRoot: handle.runsRoot,
17
+ close: async () => {
18
+ handle.close();
19
+ },
145
20
  };
146
21
  }
147
22
  //# sourceMappingURL=api-server.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"api-server.js","sourceRoot":"","sources":["../src/api-server.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-server.js","sourceRoot":"","sources":["../launcher/api-server.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,IAAI,4BAA4B,EAAsC,MAAM,WAAW,CAAC;AAYvH;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAAC,UAAmC,EAAE;IAC9E,MAAM,MAAM,GAAG,4BAA4B,CAAC;QACxC,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,IAAI,EAAE,OAAO,CAAC,IAAI;QAClB,QAAQ,EAAE,OAAO,CAAC,QAAQ;KAC7B,CAAC,CAAC;IACH,OAAO;QACH,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,IAAI,EAAE,MAAM,CAAC,IAAI;QACjB,GAAG,EAAE,MAAM,CAAC,GAAG;QACf,QAAQ,EAAE,MAAM,CAAC,QAAQ;QACzB,KAAK,EAAE,KAAK,IAAI,EAAE;YACd,MAAM,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;KACJ,CAAC;AACN,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- export { createInspectApiServer, type InspectApiServer, type InspectApiServerOptions } from './api-server.js';
1
+ export { createInspectApiServer, type InspectApiServer, type InspectApiServerOptions, } from './api-server.js';
2
2
  export { createStudio, type StartStudioOptions, type StudioProcess, startStudio, studioVersion } from './start.js';
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../launcher/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,sBAAsB,EACtB,KAAK,gBAAgB,EACrB,KAAK,uBAAuB,GAC/B,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,KAAK,kBAAkB,EAAE,KAAK,aAAa,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- export { createInspectApiServer } from './api-server.js';
1
+ export { createInspectApiServer, } from './api-server.js';
2
2
  export { createStudio, startStudio, studioVersion } from './start.js';
3
3
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAuD,MAAM,iBAAiB,CAAC;AAC9G,OAAO,EAAE,YAAY,EAA+C,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../launcher/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACH,sBAAsB,GAGzB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAA+C,WAAW,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/start.d.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  import { type ChildProcess } from 'node:child_process';
2
2
  import { type InspectApiServer, type InspectApiServerOptions } from './api-server.js';
3
3
  export type StartStudioOptions = InspectApiServerOptions & {
4
- /** Start the VMZ dev server for `projects/studio`. Default true. */
5
- ui?: boolean;
6
- /** VMZ dev port; 0 picks a free port when supported. */
7
- uiPort?: number;
4
+ /** Start VMZ watch serve for browser WebUI. Default true. */
5
+ webui?: boolean;
6
+ /** VMZ dev port; omit to auto-pick from 5173. */
7
+ webuiPort?: number;
8
8
  };
9
9
  export type StudioProcess = InspectApiServer & {
10
- ui?: ChildProcess;
10
+ webui?: ChildProcess;
11
+ webuiUrl?: string;
11
12
  closeAll: () => Promise<void>;
12
13
  };
13
14
  /**
14
- * Start loopback inspect API and optionally the VMZ Studio dev app.
15
- * Binds API to 127.0.0.1 by default.
15
+ * `dxo studio`: Rust inspect API + VMZ watch serve open in browser.
16
+ * Desktop GUI is `dxo-studio.exe` (Tauri), not this path.
16
17
  */
17
18
  export declare function startStudio(options?: StartStudioOptions): Promise<StudioProcess>;
18
19
  /** @deprecated Use {@link startStudio}. */
@@ -1 +1 @@
1
- {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAI9D,OAAO,EAA0B,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE9G,MAAM,MAAM,kBAAkB,GAAG,uBAAuB,GAAG;IACvD,oEAAoE;IACpE,EAAE,CAAC,EAAE,OAAO,CAAC;IACb,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG;IAC3C,EAAE,CAAC,EAAE,YAAY,CAAC;IAClB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,CAAC;AA0BF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,aAAa,CAAC,CAsB1F;AAED,2CAA2C;AAC3C,wBAAgB,YAAY,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAEhH;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
1
+ {"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../launcher/start.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,YAAY,EAAS,MAAM,oBAAoB,CAAC;AAI9D,OAAO,EAA0B,KAAK,gBAAgB,EAAE,KAAK,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAE9G,MAAM,MAAM,kBAAkB,GAAG,uBAAuB,GAAG;IACvD,6DAA6D;IAC7D,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,iDAAiD;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG,gBAAgB,GAAG;IAC3C,KAAK,CAAC,EAAE,YAAY,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACjC,CAAC;AA6BF;;;GAGG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,kBAAuB,GAAG,OAAO,CAAC,aAAa,CAAC,CA2B1F;AAED,2CAA2C;AAC3C,wBAAgB,YAAY,CAAC,OAAO,GAAE,kBAAuB,GAAG;IAAE,IAAI,EAAE,UAAU,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,CAEhH;AAED,wBAAgB,aAAa,IAAI,MAAM,CAEtC"}
package/dist/start.js CHANGED
@@ -4,7 +4,6 @@ import path from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { createInspectApiServer } from './api-server.js';
6
6
  const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
7
- const studioAppRoot = path.resolve(packageRoot, '../../studio');
8
7
  function spawnVmzDev(apiUrl, port) {
9
8
  const env = {
10
9
  ...process.env,
@@ -13,20 +12,24 @@ function spawnVmzDev(apiUrl, port) {
13
12
  };
14
13
  const isWin = process.platform === 'win32';
15
14
  const cmd = isWin ? 'pnpm.cmd' : 'pnpm';
16
- return spawn(cmd, ['exec', 'vmz', 'dev', '.'], {
17
- cwd: studioAppRoot,
15
+ const args = ['exec', 'vmz', 'dev', '.'];
16
+ if (port) {
17
+ args.push('--port', String(port));
18
+ }
19
+ return spawn(cmd, args, {
20
+ cwd: packageRoot,
18
21
  env,
19
22
  stdio: 'inherit',
20
23
  shell: isWin,
21
24
  });
22
25
  }
23
26
  async function writeStudioApiBootstrap(apiUrl) {
24
- const out = path.join(studioAppRoot, 'public', 'dxo-studio-api.js');
27
+ const out = path.join(packageRoot, 'public', 'dxo-studio-api.js');
25
28
  await writeFile(out, `window.__DXO_STUDIO_API__=${JSON.stringify(apiUrl)};\n`, 'utf8');
26
29
  }
27
30
  /**
28
- * Start loopback inspect API and optionally the VMZ Studio dev app.
29
- * Binds API to 127.0.0.1 by default.
31
+ * `dxo studio`: Rust inspect API + VMZ watch serve open in browser.
32
+ * Desktop GUI is `dxo-studio.exe` (Tauri), not this path.
30
33
  */
31
34
  export async function startStudio(options = {}) {
32
35
  const api = await createInspectApiServer({
@@ -34,19 +37,23 @@ export async function startStudio(options = {}) {
34
37
  port: options.port ?? 4310,
35
38
  runsRoot: options.runsRoot,
36
39
  });
37
- const uiEnabled = options.ui !== false;
38
- let ui;
39
- if (uiEnabled) {
40
+ const webuiEnabled = options.webui !== false;
41
+ let webui;
42
+ let webuiUrl;
43
+ if (webuiEnabled) {
40
44
  await writeStudioApiBootstrap(api.url);
41
- ui = spawnVmzDev(api.url, options.uiPort);
45
+ webui = spawnVmzDev(api.url, options.webuiPort);
46
+ webuiUrl = options.webuiPort
47
+ ? `http://127.0.0.1:${options.webuiPort}`
48
+ : 'http://127.0.0.1:5173 (VMZ default; check terminal if port differs)';
42
49
  }
43
50
  const closeAll = async () => {
44
- if (ui && !ui.killed) {
45
- ui.kill();
51
+ if (webui && !webui.killed) {
52
+ webui.kill();
46
53
  }
47
54
  await api.close();
48
55
  };
49
- return { ...api, ui, closeAll };
56
+ return { ...api, webui, webuiUrl, closeAll };
50
57
  }
51
58
  /** @deprecated Use {@link startStudio}. */
52
59
  export function createStudio(options = {}) {
package/dist/start.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"start.js","sourceRoot":"","sources":["../src/start.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"start.js","sourceRoot":"","sources":["../launcher/start.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@dxo/studio",
3
- "version": "0.0.9",
3
+ "version": "0.0.11",
4
4
  "type": "module",
5
- "description": "DXO training / model workbench (VMZ app placeholder, API unstable)",
6
- "license": "MIT OR Apache-2.0",
5
+ "description": "DXO training workbench VMZ WebUI + launcher; desktop GUI is dxo-studio.exe",
6
+ "license": "Apache-2.0",
7
7
  "main": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
9
  "exports": {
@@ -14,27 +14,43 @@
14
14
  },
15
15
  "files": [
16
16
  "dist",
17
+ "src",
18
+ "public",
19
+ "designs",
20
+ "vmz.config.ts",
17
21
  "README.md"
18
22
  ],
19
23
  "scripts": {
20
- "build": "tsc -p tsconfig.json"
24
+ "build": "tsc -p tsconfig.json",
25
+ "build:ui": "vmz build --release --profile static .",
26
+ "dev": "vmz dev .",
27
+ "check": "vmz check ."
21
28
  },
22
29
  "dependencies": {
23
- "@dxo/graph": "0.0.9",
24
- "@dxo/inspect": "0.0.9"
30
+ "@dxo/core": "0.0.11",
31
+ "@dxo/graph": "0.0.11",
32
+ "@dxo/inspect": "0.0.11",
33
+ "@vmz/core": "^0.1.12"
25
34
  },
26
35
  "keywords": [
27
36
  "dxo",
28
37
  "studio",
29
38
  "tensorboard",
30
39
  "vmz",
31
- "typescript"
40
+ "tauri",
41
+ "typescript",
42
+ "deep-learning"
32
43
  ],
33
- "publishConfig": {
34
- "access": "public"
35
- },
44
+ "homepage": "https://github.com/ai4waifu/dxo-framework#readme",
36
45
  "repository": {
37
46
  "type": "git",
38
- "url": "git+https://github.com/ai4waifu/dxo-framework.git"
47
+ "url": "https://github.com/ai4waifu/dxo-framework.git",
48
+ "directory": "projects/runtimes/dxo-studio"
49
+ },
50
+ "bugs": {
51
+ "url": "https://github.com/ai4waifu/dxo-framework/issues"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
39
55
  }
40
56
  }
@@ -0,0 +1,2 @@
1
+ // Overwritten by `dxo studio` / Tauri shell with the bound loopback API URL.
2
+ window.__DXO_STUDIO_API__ = 'http://127.0.0.1:4310';
@@ -0,0 +1,8 @@
1
+ <template>
2
+ <script src="/dxo-studio-api.js"></script>
3
+ <slot />
4
+ </template>
5
+
6
+ <script client>
7
+ export default class Application {}
8
+ </script>
package/src/lib/api.ts ADDED
@@ -0,0 +1,32 @@
1
+ /** Default Studio API base (loopback). Override with window.__DXO_STUDIO_API__. */
2
+ export function studioApiBase(): string {
3
+ const w = globalThis as typeof globalThis & { __DXO_STUDIO_API__?: string };
4
+ if (typeof w.__DXO_STUDIO_API__ === 'string' && w.__DXO_STUDIO_API__.length > 0) {
5
+ return w.__DXO_STUDIO_API__.replace(/\/$/, '');
6
+ }
7
+ return 'http://127.0.0.1:4310';
8
+ }
9
+
10
+ export async function apiGet<T>(path: string): Promise<T> {
11
+ const url = `${studioApiBase()}${path.startsWith('/') ? path : `/${path}`}`;
12
+ const res = await fetch(url);
13
+ if (!res.ok) {
14
+ throw new Error(`Studio API ${res.status} for ${path}`);
15
+ }
16
+ return (await res.json()) as T;
17
+ }
18
+
19
+ /** Loopback URL for run artifact bytes (images, etc.). */
20
+ export function fileUrl(runId: string, rel: string): string {
21
+ const encoded = rel.split('/').map(encodeURIComponent).join('/');
22
+ return `${studioApiBase()}/api/runs/${encodeURIComponent(runId)}/files/${encoded}`;
23
+ }
24
+
25
+ /** Default `@dxo/ui` model-app serve base for Playground. */
26
+ export function modelAppBase(): string {
27
+ const w = globalThis as typeof globalThis & { __DXO_MODEL_APP__?: string };
28
+ if (typeof w.__DXO_MODEL_APP__ === 'string' && w.__DXO_MODEL_APP__.length > 0) {
29
+ return w.__DXO_MODEL_APP__.replace(/\/$/, '');
30
+ }
31
+ return 'http://127.0.0.1:7860';
32
+ }
@@ -0,0 +1,117 @@
1
+ <template>
2
+ <main class="studio">
3
+ <header class="studio__header">
4
+ <a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
5
+ <nav class="studio__nav">
6
+ <a href="/">Runs</a>
7
+ <a href="/compare">Compare</a>
8
+ <a href="/playground">Playground</a>
9
+ </nav>
10
+ </header>
11
+
12
+ <section class="studio__panel">
13
+ <h1>Compare runs</h1>
14
+ <p class="studio__hint">Select two or more runs to overlay scalar metrics.</p>
15
+ <p class="studio__status">{status}</p>
16
+
17
+ <ul class="studio__list studio__pick-list">
18
+ <li each={runs} as="run" key={run.runId}>
19
+ <label>
20
+ <input type="checkbox" checked={isSelected(run.runId)} onChange={() => toggleRun(run.runId)} />
21
+ {runLabel(run)}
22
+ <span class="studio__badge">{run.meta.status}</span>
23
+ </label>
24
+ </li>
25
+ </ul>
26
+
27
+ <button class="studio__btn" onClick={loadCompare} disabled={selected.length < 2}>Compare</button>
28
+
29
+ <div class="studio__compare" if={compareRows.length > 0}>
30
+ <h2>Metric overlay</h2>
31
+ <table class="studio__table">
32
+ <thead>
33
+ <tr>
34
+ <th>run</th>
35
+ <th>metric</th>
36
+ <th>step</th>
37
+ <th>value</th>
38
+ </tr>
39
+ </thead>
40
+ <tbody>
41
+ <tr each={compareRows} as="row" key={row.key}>
42
+ <td>{row.runLabel}</td>
43
+ <td>{row.name}</td>
44
+ <td>{row.step}</td>
45
+ <td>{row.value}</td>
46
+ </tr>
47
+ </tbody>
48
+ </table>
49
+ </div>
50
+ </section>
51
+ </main>
52
+ </template>
53
+
54
+ <script client>
55
+ import { apiGet } from '../lib/api.ts';
56
+
57
+ export default class ComparePage {
58
+ status = 'loading…';
59
+ runs = [];
60
+ selected = [];
61
+ compareRows = [];
62
+
63
+ runLabel(run) {
64
+ return run.meta?.label ?? run.runId;
65
+ }
66
+
67
+ isSelected(runId) {
68
+ return this.selected.includes(runId);
69
+ }
70
+
71
+ toggleRun(runId) {
72
+ if (this.selected.includes(runId)) {
73
+ this.selected = this.selected.filter((id) => id !== runId);
74
+ } else {
75
+ this.selected = [...this.selected, runId];
76
+ }
77
+ }
78
+
79
+ async onMount() {
80
+ try {
81
+ const data = await apiGet('/api/runs');
82
+ this.runs = data.runs ?? [];
83
+ this.selected = this.runs.slice(0, Math.min(2, this.runs.length)).map((r) => r.runId);
84
+ this.status = `${this.runs.length} run(s) available`;
85
+ } catch (err) {
86
+ this.status = err instanceof Error ? err.message : String(err);
87
+ }
88
+ }
89
+
90
+ async loadCompare() {
91
+ if (this.selected.length < 2) return;
92
+ this.status = 'loading compare…';
93
+ try {
94
+ const q = encodeURIComponent(this.selected.join(','));
95
+ const data = await apiGet(`/api/compare?runs=${q}`);
96
+ const rows = [];
97
+ for (const s of data.series ?? []) {
98
+ const label = s.meta?.label ?? s.runId;
99
+ for (const m of s.metrics ?? []) {
100
+ rows.push({
101
+ key: `${s.runId}:${m.name}:${m.step}`,
102
+ runLabel: label,
103
+ name: m.name,
104
+ step: m.step ?? '—',
105
+ value: Number(m.value).toFixed(6),
106
+ });
107
+ }
108
+ }
109
+ this.compareRows = rows;
110
+ this.status = rows.length ? `${rows.length} metric point(s)` : 'no metrics in selected runs';
111
+ } catch (err) {
112
+ this.status = err instanceof Error ? err.message : String(err);
113
+ this.compareRows = [];
114
+ }
115
+ }
116
+ }
117
+ </script>
@@ -0,0 +1,60 @@
1
+ <template>
2
+ <main class="studio">
3
+ <header class="studio__header">
4
+ <a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
5
+ <nav class="studio__nav">
6
+ <a href="/">Runs</a>
7
+ <a href="/compare">Compare</a>
8
+ <a href="/playground">Playground</a>
9
+ </nav>
10
+ </header>
11
+
12
+ <section class="studio__panel">
13
+ <h1>Runs</h1>
14
+ <p class="studio__hint">Append-only inspect store — refresh reloads history after the trainer exits.</p>
15
+ <p class="studio__status">{status}</p>
16
+ <ul class="studio__list">
17
+ <li each={runs} as="run" key={run.runId}>
18
+ <a href={`/runs/${run.runId}`}>{runLabel(run)}</a>
19
+ <span class="studio__badge">{run.meta.status}</span>
20
+ <span class="studio__meta">{formatTime(run.meta.startedAtMs)}</span>
21
+ </li>
22
+ </ul>
23
+ </section>
24
+ </main>
25
+ </template>
26
+
27
+ <script client>
28
+ import { apiGet } from '../lib/api.ts';
29
+
30
+ export default class RunsPage {
31
+ status = 'loading…';
32
+ runs = [];
33
+
34
+ runLabel(run) {
35
+ return run.meta?.label ?? run.runId;
36
+ }
37
+
38
+ formatTime(ms) {
39
+ try {
40
+ return new Date(ms).toLocaleString();
41
+ } catch {
42
+ return String(ms);
43
+ }
44
+ }
45
+
46
+ async onMount() {
47
+ try {
48
+ const data = await apiGet('/api/runs');
49
+ this.runs = data.runs ?? [];
50
+ this.status =
51
+ this.runs.length === 0
52
+ ? 'No runs yet. Train with inspect RunRecorder, then refresh.'
53
+ : `${this.runs.length} run(s)`;
54
+ } catch (err) {
55
+ this.status = err instanceof Error ? err.message : String(err);
56
+ this.runs = [];
57
+ }
58
+ }
59
+ }
60
+ </script>
@@ -0,0 +1,91 @@
1
+ <template>
2
+ <main class="studio">
3
+ <header class="studio__header">
4
+ <a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
5
+ <nav class="studio__nav">
6
+ <a href="/">Runs</a>
7
+ <a href="/compare">Compare</a>
8
+ <a href="/playground">Playground</a>
9
+ </nav>
10
+ </header>
11
+
12
+ <section class="studio__panel studio__upload">
13
+ <h1>Image playground</h1>
14
+ <p class="studio__hint">
15
+ Upload an image to a loopback `@dxo/ui` model app (`defineModelApp`). Start serve separately on {appBase}.
16
+ </p>
17
+ <p class="studio__status">{status}</p>
18
+
19
+ <label class="studio__file">
20
+ <span>Image file</span>
21
+ <input type="file" accept="image/*" onChange={onFile} />
22
+ </label>
23
+
24
+ <button class="studio__btn" onClick={predict} disabled={!file}>Predict</button>
25
+ <button class="studio__btn studio__btn--ghost" onClick={cancel} disabled={!busy}>Cancel</button>
26
+
27
+ <div if={previewUrl} class="studio__preview">
28
+ <img src={previewUrl} alt="upload preview" />
29
+ </div>
30
+
31
+ <pre class="studio__pre" if={resultText}>{resultText}</pre>
32
+ </section>
33
+ </main>
34
+ </template>
35
+
36
+ <script client>
37
+ import { modelAppBase } from '../lib/api.ts';
38
+
39
+ export default class PlaygroundPage {
40
+ status = 'ready';
41
+ appBase = modelAppBase();
42
+ file = null;
43
+ previewUrl = '';
44
+ resultText = '';
45
+ busy = false;
46
+ #controller = null;
47
+
48
+ onFile(e) {
49
+ const input = e.target;
50
+ const f = input?.files?.[0];
51
+ this.file = f ?? null;
52
+ if (this.previewUrl) URL.revokeObjectURL(this.previewUrl);
53
+ this.previewUrl = f ? URL.createObjectURL(f) : '';
54
+ this.resultText = '';
55
+ }
56
+
57
+ async predict() {
58
+ if (!this.file) return;
59
+ this.#controller?.abort();
60
+ this.#controller = new AbortController();
61
+ this.busy = true;
62
+ this.status = 'predicting…';
63
+ this.resultText = '';
64
+ try {
65
+ const form = new FormData();
66
+ form.append('image', this.file);
67
+ const res = await fetch(`${this.appBase}/run`, {
68
+ method: 'POST',
69
+ body: form,
70
+ signal: this.#controller.signal,
71
+ });
72
+ if (!res.ok) throw new Error(`model app ${res.status}`);
73
+ const body = await res.json();
74
+ this.resultText = JSON.stringify(body, null, 2);
75
+ this.status = 'done';
76
+ } catch (err) {
77
+ if (err instanceof Error && err.name === 'AbortError') {
78
+ this.status = 'cancelled';
79
+ } else {
80
+ this.status = err instanceof Error ? err.message : String(err);
81
+ }
82
+ } finally {
83
+ this.busy = false;
84
+ }
85
+ }
86
+
87
+ cancel() {
88
+ this.#controller?.abort();
89
+ }
90
+ }
91
+ </script>
@@ -0,0 +1,295 @@
1
+ <template>
2
+ <main class="studio">
3
+ <header class="studio__header">
4
+ <a class="studio__brand" href="/"><strong>DXO Studio</strong></a>
5
+ <nav class="studio__nav">
6
+ <a href="/">Runs</a>
7
+ <a href="/compare">Compare</a>
8
+ <a href="/playground">Playground</a>
9
+ </nav>
10
+ </header>
11
+
12
+ <section class="studio__panel">
13
+ <h1>Run {runId}</h1>
14
+ <p class="studio__status">{status}</p>
15
+
16
+ <h2>Metrics</h2>
17
+ <table class="studio__table">
18
+ <thead>
19
+ <tr><th>name</th><th>step</th><th>value</th></tr>
20
+ </thead>
21
+ <tbody>
22
+ <tr each={metrics} as="m" key={metricKey(m)}>
23
+ <td>{m.name}</td>
24
+ <td>{m.step ?? '—'}</td>
25
+ <td>{formatValue(m.value)}</td>
26
+ </tr>
27
+ </tbody>
28
+ </table>
29
+
30
+ <h2>Confusion matrix</h2>
31
+ <p class="studio__hint" if={!hasMatrix}>No confusion-matrix artifact.</p>
32
+ <table class="studio__table studio__matrix" if={hasMatrix}>
33
+ <thead>
34
+ <tr>
35
+ <th></th>
36
+ <th each={matrixLabels} as="lbl" key={lbl}>{lbl}</th>
37
+ </tr>
38
+ </thead>
39
+ <tbody>
40
+ <tr each={matrixRows} as="row" key={row.label}>
41
+ <th>{row.label}</th>
42
+ <td each={row.cells} as="cell" key={cell.key} class={cellClass(cell.value)}>{cell.value}</td>
43
+ </tr>
44
+ </tbody>
45
+ </table>
46
+
47
+ <h2>Image samples</h2>
48
+ <p class="studio__hint" if={samples.length === 0}>No image-samples artifact.</p>
49
+ <ul class="studio__samples" if={samples.length > 0}>
50
+ <li each={samples} as="s" key={s.uri}>
51
+ <img src={s.src} alt={s.caption} />
52
+ <span class="studio__meta">{s.caption}</span>
53
+ </li>
54
+ </ul>
55
+
56
+ <h2>Training events</h2>
57
+ <ul class="studio__list studio__list--compact">
58
+ <li each={trainEvents} as="e" key={eventKey(e)}><code>{e.type}</code> {summarize(e)}</li>
59
+ </ul>
60
+
61
+ <h2>Checkpoints</h2>
62
+ <ul class="studio__list">
63
+ <li each={checkpoints} as="c" key={c.uri}><code>{c.name}</code> · {c.uri}</li>
64
+ </ul>
65
+
66
+ <h2>Models</h2>
67
+ <p class="studio__hint">Module · Execution · Kernel are separate views — never one merged architecture diagram.</p>
68
+ <div class="studio__tabs">
69
+ <button class={tabClass('module')} onClick={() => selectGraphTab('module')}>Module</button>
70
+ <button class={tabClass('execution')} onClick={() => selectGraphTab('execution')}>Execution</button>
71
+ <button class={tabClass('kernel')} onClick={() => selectGraphTab('kernel')}>Kernel</button>
72
+ </div>
73
+ <p class="studio__meta" if={graphAvailability}>{graphAvailability}</p>
74
+ <pre class="studio__pre">{graphText}</pre>
75
+
76
+ <h2>Profiler</h2>
77
+ <p class="studio__hint" if={!hasProfile}>{profileHint}</p>
78
+ <ul class="studio__list studio__list--compact" if={hasProfile}>
79
+ <li each={profileSpans} as="s" key={spanKey(s)}>
80
+ <code>{s.category}</code> {s.name} · {s.startMs}–{s.endMs} ms
81
+ </li>
82
+ </ul>
83
+ </section>
84
+ </main>
85
+ </template>
86
+
87
+ <script client>
88
+ import { apiGet, fileUrl } from '../../lib/api.ts';
89
+
90
+ export default class RunDetailPage {
91
+ runId = '';
92
+ status = 'loading…';
93
+ metrics = [];
94
+ trainEvents = [];
95
+ checkpoints = [];
96
+ graphTab = 'module';
97
+ graphBundle = null;
98
+ graphText = '(none)';
99
+ graphAvailability = '';
100
+ hasMatrix = false;
101
+ matrixLabels = [];
102
+ matrixRows = [];
103
+ samples = [];
104
+ hasProfile = false;
105
+ profileHint = 'No profile-trace artifact (Titan spans required; timeline is never fabricated).';
106
+ profileSpans = [];
107
+
108
+ metricKey(m) {
109
+ return `${m.name}:${m.step ?? ''}:${m.wallTimeMs ?? ''}`;
110
+ }
111
+
112
+ eventKey(e) {
113
+ return `${e.type}:${e.wallTimeMs ?? ''}:${e.step ?? ''}:${e.epoch ?? ''}`;
114
+ }
115
+
116
+ spanKey(s) {
117
+ return `${s.category}:${s.name}:${s.startMs}:${s.endMs}`;
118
+ }
119
+
120
+ tabClass(tab) {
121
+ return this.graphTab === tab ? 'studio__tab studio__tab--active' : 'studio__tab';
122
+ }
123
+
124
+ selectGraphTab(tab) {
125
+ this.graphTab = tab;
126
+ this.renderGraphTab();
127
+ }
128
+
129
+ renderGraphTab() {
130
+ const bundle = this.graphBundle;
131
+ if (!bundle) {
132
+ this.graphText = '(no model-graph artifact)';
133
+ this.graphAvailability = '';
134
+ return;
135
+ }
136
+ const g = bundle[this.graphTab] ?? bundle.module;
137
+ const avail = g.availability === 'unavailable'
138
+ ? `unavailable: ${g.unavailableReason ?? 'n/a'}`
139
+ : `view=${g.view} · nodes=${(g.nodes ?? []).length}`;
140
+ this.graphAvailability = avail;
141
+ this.graphText = JSON.stringify(g, null, 2);
142
+ }
143
+
144
+ formatValue(v) {
145
+ return Number(v).toFixed(6);
146
+ }
147
+
148
+ cellClass(v) {
149
+ const n = Number(v);
150
+ if (n >= 5) return 'studio__cell--hot';
151
+ if (n >= 2) return 'studio__cell--warm';
152
+ return '';
153
+ }
154
+
155
+ summarize(e) {
156
+ if (e.type === 'train/batch') return `epoch=${e.epoch} step=${e.step} loss=${e.loss}`;
157
+ if (e.type === 'train/epoch_end') return `epoch=${e.epoch} meanLoss=${e.meanLoss}`;
158
+ if (e.type === 'run/end') return `status=${e.status}`;
159
+ return '';
160
+ }
161
+
162
+ buildMatrix(data) {
163
+ const labels = data.labels ?? [];
164
+ const matrix = data.matrix ?? [];
165
+ this.matrixLabels = labels;
166
+ this.matrixRows = labels.map((label, i) => ({
167
+ label,
168
+ cells: (matrix[i] ?? []).map((value, j) => ({
169
+ key: `${i}:${j}`,
170
+ value,
171
+ })),
172
+ }));
173
+ this.hasMatrix = labels.length > 0;
174
+ }
175
+
176
+ buildSamples(data) {
177
+ const list = data.samples ?? [];
178
+ this.samples = list.map((s) => {
179
+ const parts = [];
180
+ if (s.label !== undefined) parts.push(`label=${s.label}`);
181
+ if (s.pred !== undefined) parts.push(`pred=${s.pred}`);
182
+ return {
183
+ uri: s.uri,
184
+ src: fileUrl(this.runId, s.uri),
185
+ caption: parts.join(' · ') || s.uri,
186
+ };
187
+ });
188
+ }
189
+
190
+ normalizeGraphArtifact(raw) {
191
+ if (!raw || typeof raw !== 'object') return null;
192
+ if (raw.format === 'dxo-model-graph-bundle') return raw;
193
+ if (raw.format === 'dxo-model-graph') {
194
+ const emptyExec = {
195
+ format: 'dxo-model-graph',
196
+ version: 0,
197
+ view: 'execution',
198
+ availability: 'unavailable',
199
+ unavailableReason: 'legacy module-only artifact',
200
+ nodes: [],
201
+ edges: [],
202
+ };
203
+ const emptyKernel = {
204
+ format: 'dxo-model-graph',
205
+ version: 0,
206
+ view: 'kernel',
207
+ availability: 'unavailable',
208
+ unavailableReason: 'legacy module-only artifact',
209
+ nodes: [],
210
+ edges: [],
211
+ };
212
+ return {
213
+ format: 'dxo-model-graph-bundle',
214
+ version: 0,
215
+ module: raw.view === 'module' ? raw : {
216
+ format: 'dxo-model-graph',
217
+ version: 0,
218
+ view: 'module',
219
+ availability: 'unavailable',
220
+ unavailableReason: 'legacy artifact was not module view',
221
+ nodes: [],
222
+ edges: [],
223
+ },
224
+ execution: raw.view === 'execution' ? raw : emptyExec,
225
+ kernel: raw.view === 'kernel' ? raw : emptyKernel,
226
+ };
227
+ }
228
+ return null;
229
+ }
230
+
231
+ async onMount() {
232
+ const parts = location.pathname.split('/').filter(Boolean);
233
+ this.runId = parts[1] ?? '';
234
+ if (!this.runId) {
235
+ this.status = 'missing run id';
236
+ return;
237
+ }
238
+ try {
239
+ const [metaRes, metricsRes, eventsRes, ckRes] = await Promise.all([
240
+ apiGet(`/api/runs/${encodeURIComponent(this.runId)}/meta`),
241
+ apiGet(`/api/runs/${encodeURIComponent(this.runId)}/metrics`),
242
+ apiGet(`/api/runs/${encodeURIComponent(this.runId)}/events`),
243
+ apiGet(`/api/runs/${encodeURIComponent(this.runId)}/checkpoints`),
244
+ ]);
245
+ this.status = metaRes.meta?.status ?? 'unknown';
246
+ this.metrics = metricsRes.metrics ?? [];
247
+ this.trainEvents = (eventsRes.events ?? []).filter(
248
+ (e) => String(e.type).startsWith('train/') || e.type === 'run/start' || e.type === 'run/end',
249
+ );
250
+ this.checkpoints = ckRes.checkpoints ?? [];
251
+ try {
252
+ const g = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/model-graph`);
253
+ this.graphBundle = this.normalizeGraphArtifact(g.graph ?? g);
254
+ this.renderGraphTab();
255
+ } catch {
256
+ this.graphBundle = null;
257
+ this.renderGraphTab();
258
+ }
259
+ try {
260
+ const cm = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/confusion-matrix`);
261
+ this.buildMatrix(cm.confusionMatrix ?? cm);
262
+ } catch {
263
+ this.hasMatrix = false;
264
+ }
265
+ try {
266
+ const imgs = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/image-samples`);
267
+ this.buildSamples(imgs.imageSamples ?? imgs);
268
+ } catch {
269
+ this.samples = [];
270
+ }
271
+ try {
272
+ const pr = await apiGet(`/api/runs/${encodeURIComponent(this.runId)}/profile-trace`);
273
+ const trace = pr.profileTrace ?? pr.trace ?? pr;
274
+ const spans = (trace.spans ?? []).filter(
275
+ (s) => s && (s.category === 'kernel' || s.category === 'transfer' || s.category === 'readback' || s.category === 'op'),
276
+ );
277
+ if (trace.availability === 'unavailable' || spans.length === 0) {
278
+ this.hasProfile = false;
279
+ this.profileHint = trace.unavailableReason
280
+ ?? 'ProfileTrace unavailable (no Titan spans; timeline not fabricated).';
281
+ this.profileSpans = [];
282
+ } else {
283
+ this.hasProfile = true;
284
+ this.profileSpans = spans;
285
+ }
286
+ } catch {
287
+ this.hasProfile = false;
288
+ this.profileHint = 'No profile-trace artifact (Titan spans required; timeline is never fabricated).';
289
+ }
290
+ } catch (err) {
291
+ this.status = err instanceof Error ? err.message : String(err);
292
+ }
293
+ }
294
+ }
295
+ </script>
package/vmz.config.ts ADDED
@@ -0,0 +1,12 @@
1
+ import { defineConfig } from '@vmz/vmz';
2
+
3
+ /** Studio WebUI — VMZ dev watch (`dxo studio`) + static build for Tauri embed. */
4
+ export default defineConfig({
5
+ delivery: {
6
+ default: 'node',
7
+ profiles: {
8
+ node: { host: 'node', assembly: 'node-ssr' },
9
+ static: { host: 'browser', assembly: 'static-cdn' },
10
+ },
11
+ },
12
+ });