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.
Files changed (2) hide show
  1. package/dist/cli.js +168 -32
  2. 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 existsSync4 } from "node:fs";
5
- import { join as join4, resolve as resolve6 } from "node:path";
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 mkdirSync2 } from "node:fs";
257
- import { dirname, resolve as resolve3 } from "node:path";
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 existsSync2, statSync } from "node:fs";
265
- import { extname, join as join2, normalize, resolve as resolve2 } from "node:path";
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 = resolve2(projectDir);
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 = join2(root, rel);
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 (existsSync2(filePath) && statSync(filePath).isDirectory()) {
307
- filePath = join2(filePath, "index.html");
422
+ if (existsSync3(filePath) && statSync(filePath).isDirectory()) {
423
+ filePath = join3(filePath, "index.html");
308
424
  }
309
- if (!existsSync2(filePath)) {
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 = resolve3(opts.projectDir);
514
+ const projectDir = resolve4(opts.projectDir);
399
515
  const stamp = new Date().toISOString().replace(/[:T]/g, "-").slice(0, 19);
400
- const out = resolve3(projectDir, opts.out ?? `renders/render_${stamp}.mp4`);
401
- mkdirSync2(dirname(out), { recursive: true });
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 existsSync3, mkdirSync as mkdirSync3, writeFileSync } from "node:fs";
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 join3, resolve as resolve4 } from "node:path";
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 = resolve4(cwd, name);
512
- if (existsSync3(join3(dir, "index.html"))) {
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
- mkdirSync3(join3(dir, "assets"), { recursive: true });
516
- writeFileSync(join3(dir, "index.html"), TEMPLATE);
517
- writeFileSync(join3(dir, ".gitignore"), `renders/
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(), join3(dir, "assets", "stereoframe.js"));
635
+ copyFileSync2(resolveRuntimeBundle(), join4(dir, "assets", "stereoframe.js"));
520
636
  return dir;
521
637
  }
522
638
  function updateRuntime(projectDir) {
523
- if (!existsSync3(join3(resolve4(projectDir), "index.html"))) {
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 = join3(resolve4(projectDir), "assets", "stereoframe.js");
527
- mkdirSync3(dirname2(target), { recursive: true });
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 resolve5 } from "node:path";
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(resolve5(projectDir));
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 = join4(resolve6(dir), "index.html");
683
- if (!existsSync4(htmlPath))
684
- throw new Error(`no index.html in ${resolve6(dir)}`);
685
- const findings = lintHtml(readFileSync(htmlPath, "utf8"), {
686
- fileExists: (rel) => existsSync4(join4(resolve6(dir), 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.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.2",
28
+ "stereoframe-runtime": "0.2.3",
29
29
  "puppeteer": "^24.0.0"
30
30
  },
31
31
  "devDependencies": {