@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 +21 -3
- package/designs/styles/studio.css +208 -0
- package/dist/api-server.d.ts +6 -8
- package/dist/api-server.d.ts.map +1 -1
- package/dist/api-server.js +16 -141
- package/dist/api-server.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/start.d.ts +8 -7
- package/dist/start.d.ts.map +1 -1
- package/dist/start.js +20 -13
- package/dist/start.js.map +1 -1
- package/package.json +27 -11
- package/public/dxo-studio-api.js +2 -0
- package/src/Application.vmz +8 -0
- package/src/lib/api.ts +32 -0
- package/src/pages/compare.vmz +117 -0
- package/src/pages/index.vmz +60 -0
- package/src/pages/playground.vmz +91 -0
- package/src/pages/runs/[id].vmz +295 -0
- package/vmz.config.ts +12 -0
package/README.md
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
1
|
# @dxo/studio
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
DXO training workbench (developer preview). **WebUI is VMZ** — not a separate Vue app.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Two entry points
|
|
6
6
|
|
|
7
|
-
|
|
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
|
+
}
|
package/dist/api-server.d.ts
CHANGED
|
@@ -1,17 +1,15 @@
|
|
|
1
|
-
import { type
|
|
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
|
-
/**
|
|
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
|
package/dist/api-server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-server.d.ts","sourceRoot":"","sources":["../
|
|
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"}
|
package/dist/api-server.js
CHANGED
|
@@ -1,147 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
139
|
-
url,
|
|
140
|
-
runsRoot,
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
package/dist/api-server.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"api-server.js","sourceRoot":"","sources":["../
|
|
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../
|
|
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
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../
|
|
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
|
|
5
|
-
|
|
6
|
-
/** VMZ dev port;
|
|
7
|
-
|
|
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
|
-
|
|
10
|
+
webui?: ChildProcess;
|
|
11
|
+
webuiUrl?: string;
|
|
11
12
|
closeAll: () => Promise<void>;
|
|
12
13
|
};
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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}. */
|
package/dist/start.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"start.d.ts","sourceRoot":"","sources":["../
|
|
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
|
-
|
|
17
|
-
|
|
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(
|
|
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
|
-
*
|
|
29
|
-
*
|
|
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
|
|
38
|
-
let
|
|
39
|
-
|
|
40
|
+
const webuiEnabled = options.webui !== false;
|
|
41
|
+
let webui;
|
|
42
|
+
let webuiUrl;
|
|
43
|
+
if (webuiEnabled) {
|
|
40
44
|
await writeStudioApiBootstrap(api.url);
|
|
41
|
-
|
|
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 (
|
|
45
|
-
|
|
51
|
+
if (webui && !webui.killed) {
|
|
52
|
+
webui.kill();
|
|
46
53
|
}
|
|
47
54
|
await api.close();
|
|
48
55
|
};
|
|
49
|
-
return { ...api,
|
|
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":["../
|
|
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.
|
|
3
|
+
"version": "0.0.11",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"description": "DXO training
|
|
6
|
-
"license": "
|
|
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/
|
|
24
|
-
"@dxo/
|
|
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
|
-
"
|
|
40
|
+
"tauri",
|
|
41
|
+
"typescript",
|
|
42
|
+
"deep-learning"
|
|
32
43
|
],
|
|
33
|
-
"
|
|
34
|
-
"access": "public"
|
|
35
|
-
},
|
|
44
|
+
"homepage": "https://github.com/ai4waifu/dxo-framework#readme",
|
|
36
45
|
"repository": {
|
|
37
46
|
"type": "git",
|
|
38
|
-
"url": "
|
|
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
|
}
|
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
|
+
});
|