demobite 1.4.0 → 1.7.0
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 +14 -13
- package/launcher/index.mjs +72 -7
- package/package.json +1 -1
- package/scripts/calibrate.mjs +17 -3
- package/scripts/record.mjs +144 -15
- package/skill/SKILL.md +126 -33
- package/skill/scripts/batch.mjs +328 -0
- package/skill/scripts/briefs.mjs +163 -9
- package/skill/scripts/cleanup.mjs +2 -1
- package/skill/scripts/manifest.mjs +13 -1
- package/skill/scripts/retake.mjs +73 -15
- package/skill/scripts/stage-wait.mjs +172 -8
- package/skill/scripts/status.mjs +44 -18
- package/skill/scripts/upload.mjs +72 -67
- package/skill/scripts/vocab.mjs +2 -1
package/skill/scripts/upload.mjs
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// DemoBites ending — STAGE the take and
|
|
2
|
+
// DemoBites ending — STAGE the take and DELIVER it.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// DIRECT DELIVERY (1.5.0, founder ruling 2026-09-25): this script zips
|
|
5
|
+
// clean.mp4, stages it together with a playable MP4, uploads both, then calls
|
|
6
|
+
// the uploaded door. The take becomes a bite by itself; the person watches it
|
|
7
|
+
// come in on their Demos grid (<base>/demos), never on the preview page. When
|
|
8
|
+
// the account has no recording minutes left the server KEEPS the take and it
|
|
9
|
+
// waits on the same grid, next to the upgrade door.
|
|
10
10
|
//
|
|
11
11
|
// Contracts (fixed, coded verbatim):
|
|
12
12
|
// PUT <base>/api/recorder/stage (Bearer)
|
|
13
13
|
// { filename, sizeBytes, previewSizeBytes, manifest }
|
|
14
14
|
// -> { stagingId, uploadUrl, previewUploadUrl, videoKey, previewUrl }
|
|
15
|
+
// PUT <base>/api/recorder/stage/<stagingId>/uploaded (Bearer) after both uploads, EVERY take
|
|
16
|
+
// -> 202 { delivered: true, biteId, studioUrl, dashboardUrl }
|
|
17
|
+
// | 403 { kept: true, waiting: "minutes", resetsAt, dashboardUrl, stagingId }
|
|
15
18
|
// GET <base>/api/recorder/stage?id=<stagingId> (Bearer)
|
|
16
19
|
// -> { status, biteId, biteUKey, biteStatus, studioUrl }
|
|
17
20
|
//
|
|
@@ -31,7 +34,7 @@ if (!dir) {
|
|
|
31
34
|
}
|
|
32
35
|
// RE-TAKE (2026-09-02): `--retake-of <biteId>` stages this take as a NEW RECORDING
|
|
33
36
|
// of an existing bite instead of a new bite. The server keeps the bite's current
|
|
34
|
-
// narration text, voice, intro/outro and look; the
|
|
37
|
+
// narration text, voice, intro/outro and look; the take is delivered into it.
|
|
35
38
|
const retakeIdx = process.argv.indexOf("--retake-of");
|
|
36
39
|
const retakeOfBiteId = retakeIdx >= 0 ? Number(process.argv[retakeIdx + 1]) : null;
|
|
37
40
|
if (retakeIdx >= 0 && !(Number.isInteger(retakeOfBiteId) && retakeOfBiteId > 0)) { console.error("--retake-of needs a bite id"); process.exit(2); }
|
|
@@ -47,11 +50,9 @@ const retakeNote = noteIdx >= 0 ? String(process.argv[noteIdx + 1] ?? "").trim()
|
|
|
47
50
|
// opts out. `--stage-only` returns right after the two uploads (the batch
|
|
48
51
|
// waits with status.mjs); `--supersede` replaces a stage already pinned to
|
|
49
52
|
// this attempt (the server refuses a second one otherwise).
|
|
50
|
-
// DELIVERY (1.3.0,
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
// and prints "delivered: bite <id>". No Approve click, no review queue for
|
|
54
|
-
// brief batches. Free-prompt takes (no attempt) still stage for review.
|
|
53
|
+
// DELIVERY (1.3.0 for brief takes, 1.5.0 for every take): after both uploads
|
|
54
|
+
// this script PUTs the delivery route (the claim's api.uploaded when there is
|
|
55
|
+
// one, fallback <base>/api/recorder/stage/<id>/uploaded). No Approve click.
|
|
55
56
|
const stageOnly = process.argv.includes("--stage-only");
|
|
56
57
|
const supersede = process.argv.includes("--supersede");
|
|
57
58
|
const attemptIdx = process.argv.indexOf("--attempt");
|
|
@@ -77,13 +78,18 @@ if (!cfg.api_key || !cfg.base) {
|
|
|
77
78
|
process.exit(1);
|
|
78
79
|
}
|
|
79
80
|
const base = cfg.base.replace(/\/+$/, "");
|
|
80
|
-
// A delivered take is a bite already
|
|
81
|
+
// A delivered take is a bite already, and a kept take waits on the server;
|
|
82
|
+
// staging either again would make a second one.
|
|
81
83
|
try {
|
|
82
84
|
const prev = JSON.parse(fs.readFileSync(path.join(dir, "staged.json"), "utf8"));
|
|
83
85
|
if (prev?.delivered === true && prev.biteId && !supersede) {
|
|
84
86
|
console.error(`${dir} was already delivered as bite ${prev.biteId}. See where it stands: node scripts/status.mjs ${dir}. For a new take of it, stage again with --supersede.`);
|
|
85
87
|
process.exit(1);
|
|
86
88
|
}
|
|
89
|
+
if (prev?.waiting && !supersede) {
|
|
90
|
+
console.error(`${dir} is already kept in DemoBites, waiting for recording minutes. See where it stands: node scripts/status.mjs ${dir}. For a new take of it, stage again with --supersede.`);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
87
93
|
} catch { /* not staged yet */ }
|
|
88
94
|
|
|
89
95
|
const cleanPath = path.join(dir, "clean.mp4");
|
|
@@ -193,7 +199,7 @@ if (!zipped) {
|
|
|
193
199
|
fs.rmSync(staging, { recursive: true, force: true });
|
|
194
200
|
const sizeBytes = fs.statSync(zipPath).size;
|
|
195
201
|
console.log(`take.zip ready (${(sizeBytes / 1024 / 1024).toFixed(1)} MB, ${zipped ? "system zip" : "store method"})`);
|
|
196
|
-
if (retakeOfBiteId) console.log(`Staging as a RE-TAKE of bite ${retakeOfBiteId} — the new recording replaces the current one inside that bite
|
|
202
|
+
if (retakeOfBiteId) console.log(`Staging as a RE-TAKE of bite ${retakeOfBiteId} — the new recording replaces the current one inside that bite by itself once delivered.`);
|
|
197
203
|
|
|
198
204
|
// ── stage ──────────────────────────────────────────────────────────────────
|
|
199
205
|
const authHeaders = { Authorization: `Bearer ${cfg.api_key}`, "Content-Type": "application/json" };
|
|
@@ -208,6 +214,11 @@ if (attempt) {
|
|
|
208
214
|
} catch (e) { console.error(`attempt event "uploading" failed: ${e.message} (continuing)`); }
|
|
209
215
|
}
|
|
210
216
|
const previewSizeBytes = fs.statSync(cleanPath).size;
|
|
217
|
+
const { deliverStaged, waitForDecision, waitForRetake, readLiveRecipeFingerprint, gridUrl, waitingLine } = await import("./stage-wait.mjs");
|
|
218
|
+
// RE-TAKE receipt (2026-09-27): the bite stays `completed` while the new
|
|
219
|
+
// version is ingested, so the wait must watch the version, not the bite. The
|
|
220
|
+
// live recipe's fingerprint BEFORE staging is the baseline it must move from.
|
|
221
|
+
const retakeBefore = retakeOfBiteId ? await readLiveRecipeFingerprint({ base, apiKey: cfg.api_key, biteId: retakeOfBiteId }) : null;
|
|
211
222
|
let stageRes;
|
|
212
223
|
try {
|
|
213
224
|
stageRes = await fetch(`${base}/api/recorder/stage`, {
|
|
@@ -228,7 +239,7 @@ if (!stageRes.ok) {
|
|
|
228
239
|
if (errBody?.error === "quota_exceeded") {
|
|
229
240
|
// Should not happen anymore — staging is quota-free by design. Neutral
|
|
230
241
|
// fallback if an older server answers this way.
|
|
231
|
-
console.error(`DemoBites declined the stage. Check ${base}/
|
|
242
|
+
console.error(`DemoBites declined the stage. Check ${base}/demos and try again.`);
|
|
232
243
|
process.exit(1);
|
|
233
244
|
}
|
|
234
245
|
if (errBody?.error === "invalid_attempt") {
|
|
@@ -236,14 +247,14 @@ if (!stageRes.ok) {
|
|
|
236
247
|
process.exit(1);
|
|
237
248
|
}
|
|
238
249
|
if (errBody?.error === "attempt_already_staged") {
|
|
239
|
-
console.error(`This attempt already has a staged take${errBody.stagingId ? ` (${errBody.stagingId})` : ""}.
|
|
250
|
+
console.error(`This attempt already has a staged take${errBody.stagingId ? ` (${errBody.stagingId})` : ""}. Keep that one, or stage again with --supersede to replace it.`);
|
|
240
251
|
process.exit(1);
|
|
241
252
|
}
|
|
242
253
|
console.error(`Stage failed: ${stageRes.status} ${errBody ? JSON.stringify(errBody) : ""}`);
|
|
243
254
|
process.exit(1);
|
|
244
255
|
}
|
|
245
256
|
const { stagingId, uploadUrl, previewUploadUrl, previewUrl, queueUrl, pendingCount } = await stageRes.json();
|
|
246
|
-
if (!stagingId || !uploadUrl || !previewUploadUrl
|
|
257
|
+
if (!stagingId || !uploadUrl || !previewUploadUrl) {
|
|
247
258
|
console.error("Stage response missing fields.");
|
|
248
259
|
process.exit(1);
|
|
249
260
|
}
|
|
@@ -263,68 +274,62 @@ async function putS3(url, contentType, filePath, label) {
|
|
|
263
274
|
await putS3(uploadUrl, "application/zip", zipPath, "ZIP");
|
|
264
275
|
await putS3(previewUploadUrl, "video/mp4", cleanPath, "Preview");
|
|
265
276
|
|
|
266
|
-
// ── delivery
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
let delivery = null;
|
|
270
|
-
if (attempt) {
|
|
271
|
-
const { deliverStaged } = await import("./stage-wait.mjs");
|
|
272
|
-
delivery = await deliverStaged({ base, apiKey: cfg.api_key, stagingId, template: uploadedTemplate });
|
|
273
|
-
}
|
|
277
|
+
// ── delivery: EVERY take becomes a bite by itself ─────────────────────────
|
|
278
|
+
const delivery = await deliverStaged({ base, apiKey: cfg.api_key, stagingId, template: uploadedTemplate });
|
|
279
|
+
const grid = gridUrl(base, delivery.dashboardUrl);
|
|
274
280
|
// The staging id used to be printed only; a batch resumes from disk, so it is
|
|
275
281
|
// persisted next to the take (status.mjs reads it, and retries a delivery
|
|
276
|
-
// that failed).
|
|
277
|
-
|
|
282
|
+
// that failed). previewUrl stays for older readers; the person's page is
|
|
283
|
+
// dashboardUrl, the Demos grid.
|
|
284
|
+
const previewPage = previewUrl ? new URL(previewUrl, base).toString() : null;
|
|
278
285
|
try {
|
|
279
286
|
fs.writeFileSync(path.join(dir, "staged.json"), JSON.stringify({
|
|
280
|
-
stagingId, previewUrl:
|
|
287
|
+
stagingId, previewUrl: previewPage, dashboardUrl: grid, queueUrl: queueUrl ? new URL(queueUrl, base).toString() : null,
|
|
281
288
|
pendingCount: pendingCount ?? null, attemptRef: attempt?.attemptRef ?? null, briefId: attempt?.briefId ?? null,
|
|
282
|
-
delivered: delivery
|
|
283
|
-
|
|
289
|
+
delivered: delivery.delivered === true, waiting: delivery.waiting ?? null, resetsAt: delivery.resetsAt ?? null,
|
|
290
|
+
biteId: delivery.biteId ?? null, videoId: delivery.videoId ?? null, studioUrl: delivery.studioUrl ? new URL(delivery.studioUrl, base).toString() : null,
|
|
291
|
+
queued: delivery.queued ?? null, pending: delivery.pending ?? null, deliveryError: delivery.error ?? null,
|
|
284
292
|
api: uploadedTemplate ? { uploaded: uploadedTemplate } : null, at: new Date().toISOString(),
|
|
293
|
+
retakeOfBiteId: retakeOfBiteId ?? null, retakeBefore,
|
|
285
294
|
}, null, 2) + "\n");
|
|
286
295
|
} catch (e) { console.error(`staged.json not written: ${e.message}`); }
|
|
287
296
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
const { waitForDecision } = await import("./stage-wait.mjs");
|
|
292
|
-
const outcome = await waitForDecision({ base, apiKey: cfg.api_key, stagingId, pageUrl, delivered: true });
|
|
293
|
-
process.exit(outcome.exitCode);
|
|
294
|
-
}
|
|
295
|
-
if (delivery && !delivery.pending) {
|
|
296
|
-
console.error(`Staged, but not delivered: ${delivery.error}. The take waits in the review queue:\n ${pageUrl}\nTry the delivery again later with: node scripts/status.mjs ${dir}`);
|
|
297
|
-
process.exit(1);
|
|
298
|
-
}
|
|
299
|
-
if (delivery?.pending) console.log("This DemoBites does not deliver by itself yet; the take waits for the word in the app.");
|
|
300
|
-
|
|
301
|
-
// ── open the in-app preview — the review happens THERE ─────────────────────
|
|
302
|
-
// Batch etiquette (founder, 2026-08-11): when takes are stacked for a later
|
|
303
|
-
// review sprint, auto-opening a tab per take is spam. `--no-open` (or
|
|
304
|
-
// config.open_preview === false) stages silently — the queue pill and the
|
|
305
|
-
// printed URL carry the message. Default stays open: for a single take the
|
|
306
|
-
// opened page IS the consent moment.
|
|
307
|
-
console.log(`Staged. Review and approve in the browser:\n ${pageUrl}`);
|
|
308
|
-
if (typeof pendingCount === "number" && pendingCount > 1 && queueUrl) {
|
|
309
|
-
console.log(`${pendingCount} takes are now waiting for review: ${new URL(queueUrl, base).toString()}`);
|
|
310
|
-
}
|
|
297
|
+
// Single take: the grid opens in the browser, the person watches the card
|
|
298
|
+
// come in there. A batch passes --no-open (or config.open_preview === false)
|
|
299
|
+
// and the printed link carries the message.
|
|
311
300
|
const noOpen = process.argv.includes("--no-open") || cfg.open_preview === false;
|
|
312
|
-
|
|
301
|
+
function openGrid() {
|
|
302
|
+
if (noOpen) return;
|
|
313
303
|
try {
|
|
314
304
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
315
|
-
spawnSync(opener, [
|
|
316
|
-
} catch { /*
|
|
305
|
+
spawnSync(opener, [grid], { stdio: "ignore" });
|
|
306
|
+
} catch { /* the printed link is the fallback */ }
|
|
317
307
|
}
|
|
318
308
|
|
|
319
|
-
if (
|
|
320
|
-
console.log(`
|
|
309
|
+
if (delivery.delivered) {
|
|
310
|
+
console.log(`Delivered. Watch it come in: ${grid}`);
|
|
311
|
+
openGrid();
|
|
312
|
+
if (stageOnly) { console.log(`Wait for the ${retakeOfBiteId ? "new recording" : "bite"} to finish later with: node scripts/status.mjs ${dir}`); process.exit(0); }
|
|
313
|
+
// LAW (founder 2026-08-08): no studio link before the bite is completed.
|
|
314
|
+
// A re-take's bite is completed already: wait for the NEW version instead.
|
|
315
|
+
const outcome = retakeOfBiteId
|
|
316
|
+
? await waitForRetake({ base, apiKey: cfg.api_key, biteId: retakeOfBiteId, videoId: delivery.videoId ?? null, manifest, before: retakeBefore, pageUrl: grid, studioUrl: delivery.studioUrl ?? null })
|
|
317
|
+
: await waitForDecision({ base, apiKey: cfg.api_key, stagingId, pageUrl: grid, delivered: true });
|
|
318
|
+
process.exit(outcome.exitCode);
|
|
319
|
+
}
|
|
320
|
+
if (delivery.waiting) {
|
|
321
|
+
console.log(waitingLine(grid, delivery.resetsAt));
|
|
322
|
+
openGrid();
|
|
321
323
|
process.exit(0);
|
|
322
324
|
}
|
|
323
|
-
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
const outcome = await waitForDecision({ base, apiKey: cfg.api_key, stagingId, pageUrl });
|
|
330
|
-
process.exit(outcome.exitCode);
|
|
325
|
+
if (delivery.pending) {
|
|
326
|
+
// An older DemoBites that does not deliver by itself: the take waits for
|
|
327
|
+
// the word in the app. Still no preview link as the destination.
|
|
328
|
+
console.log(`Staged. This DemoBites does not deliver by itself yet; the take waits for the word in the app. Watch it here: ${grid}`);
|
|
329
|
+
openGrid();
|
|
330
|
+
if (stageOnly) { console.log(`Wait for it later with: node scripts/status.mjs ${dir}`); process.exit(0); }
|
|
331
|
+
const outcome = await waitForDecision({ base, apiKey: cfg.api_key, stagingId, pageUrl: grid });
|
|
332
|
+
process.exit(outcome.exitCode);
|
|
333
|
+
}
|
|
334
|
+
console.error(`Staged, but not delivered: ${delivery.error}. Try the delivery again with: node scripts/status.mjs ${dir}\nYour demos: ${grid}`);
|
|
335
|
+
process.exit(1);
|
package/skill/scripts/vocab.mjs
CHANGED
|
@@ -24,7 +24,8 @@ if (!dir || urls.length === 0) {
|
|
|
24
24
|
process.exit(2);
|
|
25
25
|
}
|
|
26
26
|
fs.mkdirSync(dir, { recursive: true });
|
|
27
|
-
|
|
27
|
+
// RECORDER_PROFILE (1.7.0): a batch's parallel legs harvest on their own seeded profile.
|
|
28
|
+
const profileDir = path.resolve(process.env.RECORDER_PROFILE || path.join(".recorder", "profile"));
|
|
28
29
|
const headless = !args.includes("--headless=false");
|
|
29
30
|
|
|
30
31
|
const ctx = await chromium.launchPersistentContext(profileDir, { channel: "chrome", headless, viewport: { width: 1920, height: 1080 } }).catch(async () =>
|