stereoframe 0.2.2 → 0.2.3
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/dist/cli.js +168 -32
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { readFileSync, existsSync as
|
|
5
|
-
import { join as
|
|
4
|
+
import { readFileSync as readFileSync2, existsSync as existsSync5 } from "node:fs";
|
|
5
|
+
import { join as join5, resolve as resolve7 } from "node:path";
|
|
6
6
|
|
|
7
7
|
// src/blocks.ts
|
|
8
8
|
import { copyFileSync, existsSync, mkdirSync } from "node:fs";
|
|
@@ -58,6 +58,122 @@ ${listBlocks()}`);
|
|
|
58
58
|
`);
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
+
// src/gen.ts
|
|
62
|
+
import { existsSync as existsSync2, mkdirSync as mkdirSync2, readFileSync, writeFileSync } from "node:fs";
|
|
63
|
+
import { join as join2, resolve as resolve2 } from "node:path";
|
|
64
|
+
var MESHY_BASE = "https://api.meshy.ai";
|
|
65
|
+
var TEST_KEY = "msy_dummy_api_key_for_test_mode_12345678";
|
|
66
|
+
function resolveKey(projectDir, explicit) {
|
|
67
|
+
if (explicit)
|
|
68
|
+
return { key: explicit, isTest: false };
|
|
69
|
+
if (process.env.MESHY_API_KEY)
|
|
70
|
+
return { key: process.env.MESHY_API_KEY, isTest: false };
|
|
71
|
+
const envPath = join2(resolve2(projectDir), ".env");
|
|
72
|
+
if (existsSync2(envPath)) {
|
|
73
|
+
const line = readFileSync(envPath, "utf8").split(`
|
|
74
|
+
`).find((l) => l.startsWith("MESHY_API_KEY="));
|
|
75
|
+
const v = line?.slice("MESHY_API_KEY=".length).trim();
|
|
76
|
+
if (v)
|
|
77
|
+
return { key: v, isTest: false };
|
|
78
|
+
}
|
|
79
|
+
return { key: TEST_KEY, isTest: true };
|
|
80
|
+
}
|
|
81
|
+
function slug(prompt) {
|
|
82
|
+
return prompt.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 40) || "model";
|
|
83
|
+
}
|
|
84
|
+
async function meshy(path, key, init) {
|
|
85
|
+
const res = await fetch(MESHY_BASE + path, {
|
|
86
|
+
...init,
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${key}`,
|
|
89
|
+
"Content-Type": "application/json",
|
|
90
|
+
...init?.headers ?? {}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
const text = await res.text();
|
|
94
|
+
if (!res.ok) {
|
|
95
|
+
throw new Error(`Meshy ${init?.method ?? "GET"} ${path} → ${res.status}: ${text.slice(0, 300)}`);
|
|
96
|
+
}
|
|
97
|
+
return text ? JSON.parse(text) : {};
|
|
98
|
+
}
|
|
99
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
100
|
+
async function pollTask(taskId, key, label) {
|
|
101
|
+
const deadline = Date.now() + 8 * 60000;
|
|
102
|
+
let lastProgress = -1;
|
|
103
|
+
while (Date.now() < deadline) {
|
|
104
|
+
const task = await meshy(`/openapi/v2/text-to-3d/${taskId}`, key);
|
|
105
|
+
const status = task.status;
|
|
106
|
+
const progress = Number(task.progress ?? 0);
|
|
107
|
+
if (progress !== lastProgress) {
|
|
108
|
+
process.stdout.write(`\r ${label}: ${status} ${progress}% `);
|
|
109
|
+
lastProgress = progress;
|
|
110
|
+
}
|
|
111
|
+
if (status === "SUCCEEDED") {
|
|
112
|
+
process.stdout.write(`
|
|
113
|
+
`);
|
|
114
|
+
return task;
|
|
115
|
+
}
|
|
116
|
+
if (status === "FAILED" || status === "CANCELED") {
|
|
117
|
+
process.stdout.write(`
|
|
118
|
+
`);
|
|
119
|
+
throw new Error(`${label} ${status}: ${task.task_error?.message ?? "unknown error"}`);
|
|
120
|
+
}
|
|
121
|
+
await sleep(3000);
|
|
122
|
+
}
|
|
123
|
+
throw new Error(`${label} timed out after 8 minutes`);
|
|
124
|
+
}
|
|
125
|
+
async function genModel(opts) {
|
|
126
|
+
const projectDir = resolve2(opts.projectDir);
|
|
127
|
+
const { key, isTest } = resolveKey(projectDir, opts.key);
|
|
128
|
+
if (isTest) {
|
|
129
|
+
console.log(`⚠ no MESHY_API_KEY found — using Meshy test mode (returns a SAMPLE model, ignores the prompt).
|
|
130
|
+
` + " Set MESHY_API_KEY in your shell or project .env for real generations (https://www.meshy.ai/settings/api).");
|
|
131
|
+
}
|
|
132
|
+
const out = resolve2(projectDir, opts.out ?? join2("assets", `${slug(opts.prompt)}.glb`));
|
|
133
|
+
mkdirSync2(join2(out, ".."), { recursive: true });
|
|
134
|
+
console.log(`generating "${opts.prompt}" → ${out}`);
|
|
135
|
+
const preview = await meshy("/openapi/v2/text-to-3d", key, {
|
|
136
|
+
method: "POST",
|
|
137
|
+
body: JSON.stringify({
|
|
138
|
+
mode: "preview",
|
|
139
|
+
prompt: opts.prompt,
|
|
140
|
+
ai_model: "latest",
|
|
141
|
+
target_formats: ["glb"],
|
|
142
|
+
should_remesh: true,
|
|
143
|
+
...opts.polycount ? { target_polycount: opts.polycount } : {}
|
|
144
|
+
})
|
|
145
|
+
});
|
|
146
|
+
const previewId = preview.result;
|
|
147
|
+
const previewTask = await pollTask(previewId, key, "preview");
|
|
148
|
+
let finalTask = previewTask;
|
|
149
|
+
if (opts.texture) {
|
|
150
|
+
const refine = await meshy("/openapi/v2/text-to-3d", key, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body: JSON.stringify({
|
|
153
|
+
mode: "refine",
|
|
154
|
+
preview_task_id: previewId,
|
|
155
|
+
enable_pbr: true,
|
|
156
|
+
target_formats: ["glb"]
|
|
157
|
+
})
|
|
158
|
+
});
|
|
159
|
+
finalTask = await pollTask(refine.result, key, "texture");
|
|
160
|
+
}
|
|
161
|
+
const glbUrl = finalTask.model_urls?.glb;
|
|
162
|
+
if (!glbUrl)
|
|
163
|
+
throw new Error("Meshy returned no GLB url");
|
|
164
|
+
const glb = await fetch(glbUrl);
|
|
165
|
+
if (!glb.ok)
|
|
166
|
+
throw new Error(`downloading GLB → ${glb.status}`);
|
|
167
|
+
writeFileSync(out, Buffer.from(await glb.arrayBuffer()));
|
|
168
|
+
const rel = out.startsWith(projectDir) ? out.slice(projectDir.length + 1) : out;
|
|
169
|
+
console.log(`
|
|
170
|
+
✓ saved ${rel}`);
|
|
171
|
+
console.log(` use it: <sf-model src="${rel}" scale="1"></sf-model>`);
|
|
172
|
+
if (isTest)
|
|
173
|
+
console.log(" (sample model — set MESHY_API_KEY to generate from your prompt)");
|
|
174
|
+
return out;
|
|
175
|
+
}
|
|
176
|
+
|
|
61
177
|
// src/lint.ts
|
|
62
178
|
import { ASSET_ATTRS, EASE_NAMES, ELEMENT_NAMES, VERB_NAMES } from "stereoframe-runtime/vocab";
|
|
63
179
|
function readAttr(attrs, name) {
|
|
@@ -253,16 +369,16 @@ function lintHtml(html, opts) {
|
|
|
253
369
|
|
|
254
370
|
// src/render.ts
|
|
255
371
|
import { spawn } from "node:child_process";
|
|
256
|
-
import { mkdirSync as
|
|
257
|
-
import { dirname, resolve as
|
|
372
|
+
import { mkdirSync as mkdirSync3 } from "node:fs";
|
|
373
|
+
import { dirname, resolve as resolve4 } from "node:path";
|
|
258
374
|
|
|
259
375
|
// src/session.ts
|
|
260
376
|
import puppeteer from "puppeteer";
|
|
261
377
|
|
|
262
378
|
// src/serve.ts
|
|
263
379
|
import { createServer } from "node:http";
|
|
264
|
-
import { createReadStream, existsSync as
|
|
265
|
-
import { extname, join as
|
|
380
|
+
import { createReadStream, existsSync as existsSync3, statSync } from "node:fs";
|
|
381
|
+
import { extname, join as join3, normalize, resolve as resolve3 } from "node:path";
|
|
266
382
|
var MIME = {
|
|
267
383
|
".html": "text/html; charset=utf-8",
|
|
268
384
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -290,7 +406,7 @@ var MIME = {
|
|
|
290
406
|
".otf": "font/otf"
|
|
291
407
|
};
|
|
292
408
|
function serveProject(projectDir, fixedPort = 0) {
|
|
293
|
-
const root =
|
|
409
|
+
const root = resolve3(projectDir);
|
|
294
410
|
const server = createServer((req, res) => {
|
|
295
411
|
const urlPath = decodeURIComponent((req.url ?? "/").split("?")[0] ?? "/");
|
|
296
412
|
if (urlPath === "/favicon.ico") {
|
|
@@ -298,15 +414,15 @@ function serveProject(projectDir, fixedPort = 0) {
|
|
|
298
414
|
return;
|
|
299
415
|
}
|
|
300
416
|
const rel = normalize(urlPath).replace(/^(\.\.[/\\])+/, "");
|
|
301
|
-
let filePath =
|
|
417
|
+
let filePath = join3(root, rel);
|
|
302
418
|
if (!filePath.startsWith(root)) {
|
|
303
419
|
res.writeHead(403).end("forbidden");
|
|
304
420
|
return;
|
|
305
421
|
}
|
|
306
|
-
if (
|
|
307
|
-
filePath =
|
|
422
|
+
if (existsSync3(filePath) && statSync(filePath).isDirectory()) {
|
|
423
|
+
filePath = join3(filePath, "index.html");
|
|
308
424
|
}
|
|
309
|
-
if (!
|
|
425
|
+
if (!existsSync3(filePath)) {
|
|
310
426
|
res.writeHead(404).end(`not found: ${urlPath}`);
|
|
311
427
|
return;
|
|
312
428
|
}
|
|
@@ -395,10 +511,10 @@ async function renderProject(opts) {
|
|
|
395
511
|
const fps = opts.fps ?? 30;
|
|
396
512
|
const crf = opts.draft ? 28 : opts.crf ?? 18;
|
|
397
513
|
const preset = opts.draft ? "veryfast" : "medium";
|
|
398
|
-
const projectDir =
|
|
514
|
+
const projectDir = resolve4(opts.projectDir);
|
|
399
515
|
const stamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19);
|
|
400
|
-
const out =
|
|
401
|
-
|
|
516
|
+
const out = resolve4(projectDir, opts.out ?? `renders/render_${stamp}.mp4`);
|
|
517
|
+
mkdirSync3(dirname(out), { recursive: true });
|
|
402
518
|
const session = await openSession(projectDir, { echoErrors: true });
|
|
403
519
|
try {
|
|
404
520
|
const { page, info } = session;
|
|
@@ -463,9 +579,9 @@ async function renderProject(opts) {
|
|
|
463
579
|
}
|
|
464
580
|
|
|
465
581
|
// src/scaffold.ts
|
|
466
|
-
import { copyFileSync as copyFileSync2, existsSync as
|
|
582
|
+
import { copyFileSync as copyFileSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
467
583
|
import { createRequire } from "node:module";
|
|
468
|
-
import { dirname as dirname2, join as
|
|
584
|
+
import { dirname as dirname2, join as join4, resolve as resolve5 } from "node:path";
|
|
469
585
|
var TEMPLATE = `<!doctype html>
|
|
470
586
|
<html lang="en">
|
|
471
587
|
<head>
|
|
@@ -508,34 +624,34 @@ function resolveRuntimeBundle() {
|
|
|
508
624
|
return require2.resolve("stereoframe-runtime");
|
|
509
625
|
}
|
|
510
626
|
function scaffoldProject(name, cwd = process.cwd()) {
|
|
511
|
-
const dir =
|
|
512
|
-
if (
|
|
627
|
+
const dir = resolve5(cwd, name);
|
|
628
|
+
if (existsSync4(join4(dir, "index.html"))) {
|
|
513
629
|
throw new Error(`refusing to overwrite existing project at ${dir}`);
|
|
514
630
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
631
|
+
mkdirSync4(join4(dir, "assets"), { recursive: true });
|
|
632
|
+
writeFileSync2(join4(dir, "index.html"), TEMPLATE);
|
|
633
|
+
writeFileSync2(join4(dir, ".gitignore"), `renders/
|
|
518
634
|
`);
|
|
519
|
-
copyFileSync2(resolveRuntimeBundle(),
|
|
635
|
+
copyFileSync2(resolveRuntimeBundle(), join4(dir, "assets", "stereoframe.js"));
|
|
520
636
|
return dir;
|
|
521
637
|
}
|
|
522
638
|
function updateRuntime(projectDir) {
|
|
523
|
-
if (!
|
|
639
|
+
if (!existsSync4(join4(resolve5(projectDir), "index.html"))) {
|
|
524
640
|
throw new Error(`no index.html in ${projectDir} — not a stereoframe project`);
|
|
525
641
|
}
|
|
526
|
-
const target =
|
|
527
|
-
|
|
642
|
+
const target = join4(resolve5(projectDir), "assets", "stereoframe.js");
|
|
643
|
+
mkdirSync4(dirname2(target), { recursive: true });
|
|
528
644
|
copyFileSync2(resolveRuntimeBundle(), target);
|
|
529
645
|
return target;
|
|
530
646
|
}
|
|
531
647
|
|
|
532
648
|
// src/validate.ts
|
|
533
|
-
import { resolve as
|
|
649
|
+
import { resolve as resolve6 } from "node:path";
|
|
534
650
|
async function validateProject(projectDir) {
|
|
535
651
|
const findings = [];
|
|
536
652
|
let session;
|
|
537
653
|
try {
|
|
538
|
-
session = await openSession(
|
|
654
|
+
session = await openSession(resolve6(projectDir));
|
|
539
655
|
} catch (err) {
|
|
540
656
|
return [
|
|
541
657
|
{
|
|
@@ -660,6 +776,12 @@ USAGE
|
|
|
660
776
|
--draft fast low-quality render for iteration
|
|
661
777
|
stereoframe preview [dir] serve with looping wall-clock playback
|
|
662
778
|
--port <n> fixed port (default: random)
|
|
779
|
+
stereoframe gen "<prompt>" generate a 3D model (GLB) from text via Meshy
|
|
780
|
+
--dir <dir> project dir (default .)
|
|
781
|
+
--out <path> output path (default assets/<slug>.glb)
|
|
782
|
+
--no-texture skip the PBR texture pass (faster, untextured mesh)
|
|
783
|
+
--polycount <n> target polygon count
|
|
784
|
+
--key <key> Meshy API key (else MESHY_API_KEY / .env / test mode)
|
|
663
785
|
stereoframe add <block> [dir] install a visual block's assets + print usage
|
|
664
786
|
stereoframe blocks list available blocks
|
|
665
787
|
stereoframe update [dir] refresh assets/stereoframe.js from the CLI's bundled runtime
|
|
@@ -679,11 +801,11 @@ async function main() {
|
|
|
679
801
|
return;
|
|
680
802
|
}
|
|
681
803
|
case "lint": {
|
|
682
|
-
const htmlPath =
|
|
683
|
-
if (!
|
|
684
|
-
throw new Error(`no index.html in ${
|
|
685
|
-
const findings = lintHtml(
|
|
686
|
-
fileExists: (rel) =>
|
|
804
|
+
const htmlPath = join5(resolve7(dir), "index.html");
|
|
805
|
+
if (!existsSync5(htmlPath))
|
|
806
|
+
throw new Error(`no index.html in ${resolve7(dir)}`);
|
|
807
|
+
const findings = lintHtml(readFileSync2(htmlPath, "utf8"), {
|
|
808
|
+
fileExists: (rel) => existsSync5(join5(resolve7(dir), rel))
|
|
687
809
|
});
|
|
688
810
|
reportFindings("lint", findings, options.get("json") === true);
|
|
689
811
|
return;
|
|
@@ -711,6 +833,20 @@ async function main() {
|
|
|
711
833
|
console.log("press ctrl-c to stop");
|
|
712
834
|
return;
|
|
713
835
|
}
|
|
836
|
+
case "gen": {
|
|
837
|
+
const prompt = positional.join(" ").trim();
|
|
838
|
+
if (!prompt)
|
|
839
|
+
throw new Error('usage: stereoframe gen "<prompt>"');
|
|
840
|
+
await genModel({
|
|
841
|
+
prompt,
|
|
842
|
+
projectDir: typeof options.get("dir") === "string" ? options.get("dir") : ".",
|
|
843
|
+
out: typeof options.get("out") === "string" ? options.get("out") : undefined,
|
|
844
|
+
texture: options.get("no-texture") !== true,
|
|
845
|
+
polycount: options.has("polycount") ? Number(options.get("polycount")) : undefined,
|
|
846
|
+
key: typeof options.get("key") === "string" ? options.get("key") : undefined
|
|
847
|
+
});
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
714
850
|
case "add": {
|
|
715
851
|
const name = positional[0];
|
|
716
852
|
if (!name)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stereoframe",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Declarative, deterministic 3D video on three.js — scaffold, lint, validate, and render compositions built for AI agents.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"test": "bun test"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"stereoframe-runtime": "0.2.
|
|
28
|
+
"stereoframe-runtime": "0.2.3",
|
|
29
29
|
"puppeteer": "^24.0.0"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|