@neta-art/cohub-cli 10.2.0 → 10.4.1
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 +40 -2
- package/dist/board-export-resources.d.ts +8 -0
- package/dist/board-export-resources.js +39 -0
- package/dist/board-export.d.ts +6 -0
- package/dist/board-export.js +58 -8
- package/dist/board-kernel.d.ts +1 -1
- package/dist/board-kernel.js +211 -141
- package/dist/commands/boards/authoring.js +14 -16
- package/dist/commands/boards.d.ts +1 -0
- package/dist/commands/boards.js +44 -13
- package/dist/commands/rooms-connect.d.ts +17 -0
- package/dist/commands/rooms-connect.js +140 -0
- package/dist/commands/rooms-rtc-binary.d.ts +2 -0
- package/dist/commands/rooms-rtc-binary.js +107 -0
- package/dist/commands/rooms-rtc.d.ts +21 -0
- package/dist/commands/rooms-rtc.js +171 -0
- package/dist/commands/rooms.js +45 -0
- package/dist/commands/sandboxd-binary.d.ts +3 -3
- package/dist/commands/sandboxd-binary.js +27 -17
- package/dist/runtime/harness.d.ts +5 -0
- package/dist/runtime/harness.js +29 -3
- package/dist/runtime/json-rpc.js +1 -1
- package/dist/runtime/launch.js +21 -6
- package/dist/runtime/native/attach.js +5 -2
- package/dist/runtime/native/codex-link.d.ts +2 -0
- package/dist/runtime/native/codex-link.js +3 -1
- package/dist/runtime/native/execution.js +3 -3
- package/dist/runtime/native/ingest.js +4 -4
- package/dist/runtime/native/paths.d.ts +2 -0
- package/dist/runtime/native/paths.js +7 -0
- package/dist/runtime/native/pi-extension.js +12 -2
- package/dist/runtime/native/pi-link.js +21 -4
- package/dist/runtime/native/version.js +4 -1
- package/dist/runtime/supervisor.js +33 -4
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -55,6 +55,36 @@ and, if needed, `COHUB_AUTH_RESOURCE` and `COHUB_AUTH_SCOPE`. Stored login sessi
|
|
|
55
55
|
Home Space caches, and Runtime directory bindings are scoped to the API base URL.
|
|
56
56
|
Sign in once for each custom server, or supply `COHUB_EXECUTION_TOKEN`.
|
|
57
57
|
|
|
58
|
+
## Board export resources
|
|
59
|
+
|
|
60
|
+
Board exports keep the requested resolution and source image quality. Before rendering,
|
|
61
|
+
`boards export` prints the output dimensions and frame count (except with `--json`).
|
|
62
|
+
Decoded image textures, including background and script output images, share a 256 MiB
|
|
63
|
+
budget. Oversized images fail before native decoding rather than silently becoming
|
|
64
|
+
placeholders. Use `--image-memory <MiB>` to raise the budget when the environment has
|
|
65
|
+
sufficient memory; it is an image budget, not a limit on total process memory.
|
|
66
|
+
`--size` controls the viewport only with `--camera`.
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
cohub boards export lesson.board --animation lecture --at 0:92s:40ms --camera --size 1600x900 -o lecture.mp4
|
|
70
|
+
NODE_DEBUG=cohub:board-export cohub boards export lesson.board --animation lecture --at 0:92s:40ms -o lecture.mp4
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Debug output reports render stages, RSS, heap, external buffers, reserved image bytes,
|
|
74
|
+
and available/constrained process memory. Video encoding is reported separately.
|
|
75
|
+
MP4 exports require even output dimensions and fail before rendering if the initial
|
|
76
|
+
export dimensions are odd. Video frame dimensions must remain constant.
|
|
77
|
+
|
|
78
|
+
For a repeatable 2,301-frame native export memory benchmark, run from the repository root:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node --import tsx packages/cli/scripts/bench-board-export.ts
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Set `BOARD_EXPORT_BENCH_FRAMES`, `BOARD_EXPORT_BENCH_WIDTH`, or
|
|
85
|
+
`BOARD_EXPORT_BENCH_HEIGHT` to change the workload. The benchmark discards PNGs
|
|
86
|
+
as they are produced and stops if RSS exceeds 1 GiB.
|
|
87
|
+
|
|
58
88
|
## Usage
|
|
59
89
|
|
|
60
90
|
Use `--json` when reading output for decisions, extracting IDs, or chaining commands.
|
|
@@ -452,8 +482,16 @@ cohub apps resolve <appSlug> --owner <username> --space-slug <spaceSlug>
|
|
|
452
482
|
|
|
453
483
|
Use `--json` for machine-readable output. `apps get`, `apps stats`, and `apps download` also accept `app://<username>/<space>/<app>` mention URIs. `apps stats` reports total, 24-hour, 7-day, and 30-day views with a source breakdown. Download restores newly published file and directory artifacts directly from the CDN with checksum verification. HTML files with companion assets are restored as directory bundles; Board and port apps are not downloadable. The resolve command remains available for explicit slug-based lookup.
|
|
454
484
|
|
|
455
|
-
|
|
456
|
-
|
|
485
|
+
Agents can join hosted Rooms and attach realtime events or media:
|
|
486
|
+
|
|
487
|
+
```bash
|
|
488
|
+
cohub rooms join '<invite>'
|
|
489
|
+
cohub rooms connect <room-id> --json
|
|
490
|
+
cohub rooms rtc <room-id> --channel chat --data-input -
|
|
491
|
+
cohub rooms rtc <room-id> --audio speech.pcm --output ./received
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
RTC uses an on-demand Go/Pion helper, with external FFmpeg only when media conversion is needed. Encoded Ogg Opus and VP8 IVF inputs avoid re-encoding. Reception defaults to framed RTP archives; `--receive-format raw` decodes PCM/I420 for models. `--output -` writes binary frames to stdout and status to stderr. Recording is explicit, files are never overwritten, and the default receive quota is 1 GiB. Disconnecting keeps the hosted seat. See [Room RTC helper](../../apps/sandbox/cmd/cohub-rtc/README.md) for local builds, formats, migration details, and release prerequisites.
|
|
457
495
|
|
|
458
496
|
## Drive the Cohub UI
|
|
459
497
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BoardNodeRenderer } from "@neta-art/cohub/board/export/node";
|
|
2
|
+
export declare const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
3
|
+
export declare class BoardExportResourceError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export declare function createBoardExportImageBudget(renderer: BoardNodeRenderer, maxBytes: number): {
|
|
6
|
+
decodeImage: (bytes: Uint8Array, mimeType?: string) => Promise<import("@neta-art/cohub/board/export/node").BoardNodeTexture>;
|
|
7
|
+
readonly usedBytes: number;
|
|
8
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export const BOARD_EXPORT_IMAGE_MEMORY_MIB = 256;
|
|
2
|
+
export class BoardExportResourceError extends Error {
|
|
3
|
+
}
|
|
4
|
+
export function createBoardExportImageBudget(renderer, maxBytes) {
|
|
5
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes <= 0)
|
|
6
|
+
throw new Error("Image memory budget must be a positive integer.");
|
|
7
|
+
let usedBytes = 0;
|
|
8
|
+
const decodeImage = async (bytes, mimeType) => {
|
|
9
|
+
const { default: sharp } = await import("sharp");
|
|
10
|
+
const { width, height } = await sharp(bytes, { limitInputPixels: false }).metadata();
|
|
11
|
+
const required = (width ?? 0) * (height ?? 0) * 4;
|
|
12
|
+
if (!Number.isSafeInteger(required) || required <= 0)
|
|
13
|
+
throw new Error("Image dimensions could not be read.");
|
|
14
|
+
if (required > maxBytes - usedBytes) {
|
|
15
|
+
throw new BoardExportResourceError(`Image ${width}x${height} needs ${Math.ceil(required / 1024 ** 2)} MiB; ` +
|
|
16
|
+
`${Math.ceil(usedBytes / 1024 ** 2)} MiB is already reserved against the ${Math.floor(maxBytes / 1024 ** 2)} MiB image budget. ` +
|
|
17
|
+
"Use smaller source images, export a smaller region, or increase --image-memory if enough memory is available.");
|
|
18
|
+
}
|
|
19
|
+
usedBytes += required;
|
|
20
|
+
try {
|
|
21
|
+
const texture = await renderer.decodeImage(bytes, mimeType);
|
|
22
|
+
const destroy = texture.destroy.bind(texture);
|
|
23
|
+
let released = false;
|
|
24
|
+
texture.destroy = (destroySource) => {
|
|
25
|
+
if (destroySource && !released) {
|
|
26
|
+
released = true;
|
|
27
|
+
usedBytes -= required;
|
|
28
|
+
}
|
|
29
|
+
destroy(destroySource);
|
|
30
|
+
};
|
|
31
|
+
return texture;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
usedBytes -= required;
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
return { decodeImage, get usedBytes() { return usedBytes; } };
|
|
39
|
+
}
|
package/dist/board-export.d.ts
CHANGED
|
@@ -38,6 +38,12 @@ export type BoardExportRunOptions = {
|
|
|
38
38
|
};
|
|
39
39
|
document?: BoardDocument;
|
|
40
40
|
signal?: AbortSignal;
|
|
41
|
+
imageMemoryMiB?: number;
|
|
42
|
+
onPlan?: (plan: {
|
|
43
|
+
width: number;
|
|
44
|
+
height: number;
|
|
45
|
+
frames: number;
|
|
46
|
+
}) => void;
|
|
41
47
|
onFrame?: (frame: BoardExportFrame, index: number) => Promise<void>;
|
|
42
48
|
onProgress?: (completed: number, total: number) => void;
|
|
43
49
|
};
|
package/dist/board-export.js
CHANGED
|
@@ -3,7 +3,7 @@ import { mkdtemp, rm, writeFile, copyFile, link, rename, unlink } from "node:fs/
|
|
|
3
3
|
import { constants } from "node:fs";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
import { execFile as execFileCallback } from "node:child_process";
|
|
6
|
-
import { promisify } from "node:util";
|
|
6
|
+
import { debuglog, promisify } from "node:util";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { dirname, join } from "node:path";
|
|
9
9
|
import { fileURLToPath } from "node:url";
|
|
@@ -15,6 +15,8 @@ import { resolveBoardId } from "./board-command-support.js";
|
|
|
15
15
|
import { BOARD_EXPORT_FONTS } from "./board-fonts.js";
|
|
16
16
|
import { createClient } from "./client.js";
|
|
17
17
|
import { downloadPublicAudio, downloadPublicImage } from "./safe-remote-image.js";
|
|
18
|
+
import { BOARD_EXPORT_IMAGE_MEMORY_MIB, BoardExportResourceError, createBoardExportImageBudget } from "./board-export-resources.js";
|
|
19
|
+
const debugExport = debuglog("cohub:board-export");
|
|
18
20
|
export function resolveBundledFonts() {
|
|
19
21
|
const require = createRequire(import.meta.url);
|
|
20
22
|
const bundled = fileURLToPath(new URL("./fonts/", import.meta.url));
|
|
@@ -66,10 +68,13 @@ export async function loadBoardTextures(renderer, spaceId, items, options = {})
|
|
|
66
68
|
if (!wanted.has(key))
|
|
67
69
|
failed.delete(key);
|
|
68
70
|
const pending = selection.keys.filter((key) => !textures.has(key) && !failed.has(key));
|
|
69
|
-
const concurrency = Math.max(1, Math.min(options.concurrency ??
|
|
71
|
+
const concurrency = Math.max(1, Math.min(options.concurrency ?? 2, 16));
|
|
70
72
|
const client = createClient();
|
|
73
|
+
let resourceError;
|
|
71
74
|
async function worker() {
|
|
72
75
|
for (;;) {
|
|
76
|
+
if (resourceError)
|
|
77
|
+
return;
|
|
73
78
|
const key = pending.shift();
|
|
74
79
|
if (!key)
|
|
75
80
|
return;
|
|
@@ -85,12 +90,17 @@ export async function loadBoardTextures(renderer, spaceId, items, options = {})
|
|
|
85
90
|
const texture = await renderer.decodeImage(bytes, mimeType);
|
|
86
91
|
textures.set(key, texture);
|
|
87
92
|
}
|
|
88
|
-
catch {
|
|
89
|
-
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof BoardExportResourceError)
|
|
95
|
+
resourceError = error;
|
|
96
|
+
else
|
|
97
|
+
failed.add(key);
|
|
90
98
|
}
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
await Promise.all(Array.from({ length: Math.min(concurrency, pending.length) }, worker));
|
|
102
|
+
if (resourceError)
|
|
103
|
+
throw resourceError;
|
|
94
104
|
return { textures, failed: [...failed], omitted: selection.omittedKeys };
|
|
95
105
|
}
|
|
96
106
|
async function readSpaceFileBytes(client, spaceId, path) {
|
|
@@ -117,7 +127,14 @@ export async function runBoardVideoExport(options) {
|
|
|
117
127
|
try {
|
|
118
128
|
let count = 0;
|
|
119
129
|
let dimensions = { width: 0, height: 0 };
|
|
120
|
-
const result = await runBoardExport({ ...options, document, format: "png",
|
|
130
|
+
const result = await runBoardExport({ ...options, document, format: "png", onPlan: (plan) => {
|
|
131
|
+
if (options.format === "mp4" && (plan.width % 2 || plan.height % 2)) {
|
|
132
|
+
throw new Error(`MP4 needs even dimensions, but the export is ${plan.width}x${plan.height}. Use --camera --size with even dimensions, or adjust the export region.`);
|
|
133
|
+
}
|
|
134
|
+
options.onPlan?.(plan);
|
|
135
|
+
}, onFrame: async (frame, index) => {
|
|
136
|
+
if (count && (frame.width !== dimensions.width || frame.height !== dimensions.height))
|
|
137
|
+
throw new Error("Video frame dimensions changed. Use a fixed export region or a smaller camera viewport.");
|
|
121
138
|
await writeFile(join(tempDir, `${String(index + 1).padStart(6, "0")}.png`), frame.bytes, { signal: options.signal });
|
|
122
139
|
dimensions = { width: frame.width, height: frame.height };
|
|
123
140
|
count++;
|
|
@@ -160,6 +177,7 @@ export async function runBoardVideoExport(options) {
|
|
|
160
177
|
args.push("-c:v", "libvpx-vp9", "-crf", "32", "-b:v", "0", "-c:a", "libopus", "-f", "webm");
|
|
161
178
|
const encoded = join(tempDir, `video.${options.format}`);
|
|
162
179
|
args.push("-t", duration.toFixed(3), encoded);
|
|
180
|
+
debugExport("%j", { stage: "encode", width: dimensions.width, height: dimensions.height, frames: count, memory: process.memoryUsage() });
|
|
163
181
|
try {
|
|
164
182
|
await execFile("ffmpeg", args, { encoding: "utf8", maxBuffer: 2 * 1024 * 1024, signal: options.signal });
|
|
165
183
|
}
|
|
@@ -195,7 +213,22 @@ export async function runBoardExport(options) {
|
|
|
195
213
|
throw new Error(`Animation ${options.animation} does not exist.`);
|
|
196
214
|
}
|
|
197
215
|
const times = options.times?.length ? options.times : [null];
|
|
198
|
-
const
|
|
216
|
+
const imageMemoryBytes = (options.imageMemoryMiB ?? BOARD_EXPORT_IMAGE_MEMORY_MIB) * 1024 ** 2;
|
|
217
|
+
if (!Number.isSafeInteger(imageMemoryBytes) || imageMemoryBytes <= 0)
|
|
218
|
+
throw new Error("Image memory budget must be a positive integer.");
|
|
219
|
+
const nodeRenderer = await createNodeBoardRenderer({ fonts: resolveBundledFonts() });
|
|
220
|
+
const images = createBoardExportImageBudget(nodeRenderer, imageMemoryBytes);
|
|
221
|
+
const renderer = { ...nodeRenderer, decodeImage: images.decodeImage };
|
|
222
|
+
const trace = (stage, completed, dimensions) => {
|
|
223
|
+
if (!debugExport.enabled || completed % 30 !== 0)
|
|
224
|
+
return;
|
|
225
|
+
debugExport("%j", {
|
|
226
|
+
stage, completed, total: times.length,
|
|
227
|
+
...(dimensions ? { width: dimensions.width, height: dimensions.height } : {}),
|
|
228
|
+
imageBytes: images.usedBytes, memory: process.memoryUsage(),
|
|
229
|
+
availableMemory: process.availableMemory(), constrainedMemory: process.constrainedMemory(),
|
|
230
|
+
});
|
|
231
|
+
};
|
|
199
232
|
let scriptHost = null;
|
|
200
233
|
const textureCache = { textures: new Map(), failed: new Set() };
|
|
201
234
|
let backgroundTexture;
|
|
@@ -210,6 +243,17 @@ export async function runBoardExport(options) {
|
|
|
210
243
|
});
|
|
211
244
|
if (!plan)
|
|
212
245
|
return null;
|
|
246
|
+
const region = times.length > 1 && !options.camera ? { kind: "rect", rect: plan.world } : options.region;
|
|
247
|
+
const firstTime = times[0] ?? null;
|
|
248
|
+
const first = prepareBoardExport(document, {
|
|
249
|
+
sampler, region, camera: options.camera, scale: options.scale,
|
|
250
|
+
padding: times.length > 1 ? 0 : options.padding,
|
|
251
|
+
...(firstTime === null ? {} : { at: { animation: options.animation, time: firstTime } }),
|
|
252
|
+
});
|
|
253
|
+
if (!first)
|
|
254
|
+
return null;
|
|
255
|
+
options.onPlan?.({ width: first.plan.width, height: first.plan.height, frames: times.length });
|
|
256
|
+
trace("plan", 0, first.plan);
|
|
213
257
|
const warnings = [];
|
|
214
258
|
const textures = textureCache.textures;
|
|
215
259
|
let omittedKeys = new Set();
|
|
@@ -222,19 +266,21 @@ export async function runBoardExport(options) {
|
|
|
222
266
|
const { bytes, mimeType } = await downloadPublicImage(declaredBackground.imageUrl);
|
|
223
267
|
backgroundTexture = await renderer.decodeImage(bytes, mimeType);
|
|
224
268
|
}
|
|
225
|
-
catch {
|
|
269
|
+
catch (error) {
|
|
270
|
+
if (error instanceof BoardExportResourceError)
|
|
271
|
+
throw error;
|
|
226
272
|
warnings.push("The board background image could not be loaded; the fallback color was exported.");
|
|
227
273
|
}
|
|
228
274
|
}
|
|
229
275
|
const videoCount = plan.items.filter((item) => item.type === "video").length;
|
|
230
276
|
if (hasBoardScripts(document))
|
|
231
277
|
scriptHost = createNodeBoardScriptHost(renderer, {
|
|
278
|
+
concurrency: 2,
|
|
232
279
|
readModule: async (src) => {
|
|
233
280
|
const { bytes } = await readSpaceFileBytes(createClient(), options.spaceId, src);
|
|
234
281
|
return new TextDecoder().decode(bytes);
|
|
235
282
|
},
|
|
236
283
|
});
|
|
237
|
-
const region = times.length > 1 && !options.camera ? { kind: "rect", rect: plan.world } : options.region;
|
|
238
284
|
const host = scriptHost;
|
|
239
285
|
if (videoCount > 0) {
|
|
240
286
|
warnings.push(`${videoCount} video preview${videoCount === 1 ? " was" : "s were"} drawn as placeholders; video decoding is unavailable in Node.`);
|
|
@@ -259,6 +305,7 @@ export async function runBoardExport(options) {
|
|
|
259
305
|
if (!initial)
|
|
260
306
|
continue;
|
|
261
307
|
let prepared = initial;
|
|
308
|
+
trace("prepare", completed, initial.plan);
|
|
262
309
|
if (host) {
|
|
263
310
|
await host.prepare(initial.scene.items, [time ?? 0], initial.plan.scale);
|
|
264
311
|
for (const item of initial.scene.items) {
|
|
@@ -271,6 +318,7 @@ export async function runBoardExport(options) {
|
|
|
271
318
|
continue;
|
|
272
319
|
prepared = scripted;
|
|
273
320
|
}
|
|
321
|
+
trace("images", completed, prepared.plan);
|
|
274
322
|
if (options.withImages) {
|
|
275
323
|
const loaded = await loadBoardTextures(renderer, options.spaceId, prepared.plan.items.filter((item) => prepared.scene.opacity(item.id) > 0), { cache: textureCache });
|
|
276
324
|
omittedKeys = new Set(loaded.omitted);
|
|
@@ -284,6 +332,7 @@ export async function runBoardExport(options) {
|
|
|
284
332
|
reported.add(warning);
|
|
285
333
|
}
|
|
286
334
|
}
|
|
335
|
+
trace("render", completed, prepared.plan);
|
|
287
336
|
const result = exportBoardImageBytes(renderer, document, {
|
|
288
337
|
...frameOptions,
|
|
289
338
|
colorScheme: options.colorScheme,
|
|
@@ -320,6 +369,7 @@ export async function runBoardExport(options) {
|
|
|
320
369
|
frames.push(frame);
|
|
321
370
|
completed++;
|
|
322
371
|
options.onProgress?.(completed, times.length);
|
|
372
|
+
trace("rendered", completed, result.plan);
|
|
323
373
|
}
|
|
324
374
|
return completed ? { frames, warnings } : null;
|
|
325
375
|
}
|
package/dist/board-kernel.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export { BOARD_SCRIPT_DRAWING_EXAMPLE, hasBoardScripts, boardDocumentAt, createBoardAnimationSampler, boardSequence, boardSequencePatch, boardPathTrack, boardImageKeySource, buildBoardScene, imageAssetKey, parseBoardDocument, planBoardExport, selectBoardExportAssets, } from "@neta-art/cohub/board";
|
|
2
2
|
export { prepareBoardExport } from "@neta-art/cohub/board/export";
|
|
3
|
-
export { createBoardRenderSampler, inspectBoardScene, measureBoardText } from "@neta-art/cohub/board/render";
|
|
3
|
+
export { createBoardRenderSampler, inspectBoardScene, inspectBoardItems, measureBoardText } from "@neta-art/cohub/board/render";
|
|
4
4
|
export { createNodeBoardRenderer, createNodeBoardScriptHost, exportBoardImageBytes } from "@neta-art/cohub/board/export/node";
|