paper-motion 0.4.1 → 0.5.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/CHANGELOG.md +78 -0
- package/bin/pamo.mjs +62 -5
- package/lib/edit.mjs +31 -3
- package/lib/piece.mjs +121 -10
- package/lib/registry.mjs +10 -4
- package/lib/studio.mjs +195 -7
- package/package.json +2 -1
- package/runtime/brand.js +14 -2
- package/runtime/player.js +4 -2
- package/studio/index.html +236 -57
- package/examples/breath/stills/contact-sheet.jpg +0 -0
- package/examples/breath/stills/t000.00.png +0 -0
- package/examples/breath/stills/t002.00.png +0 -0
- package/examples/breath/stills/t004.00.png +0 -0
- package/examples/breath/stills/t006.00.png +0 -0
- package/examples/breath/stills/t008.00.png +0 -0
- package/examples/breath/stills/t010.00.png +0 -0
- package/examples/chrome-endcard/stills/contact-sheet.jpg +0 -0
- package/examples/chrome-endcard/stills/t000.00.png +0 -0
- package/examples/chrome-endcard/stills/t001.33.png +0 -0
- package/examples/chrome-endcard/stills/t002.67.png +0 -0
- package/examples/chrome-endcard/stills/t004.00.png +0 -0
- package/examples/chrome-endcard/stills/t005.33.png +0 -0
- package/examples/chrome-endcard/stills/t006.67.png +0 -0
- package/examples/halo-explainer/stills/contact-sheet.jpg +0 -0
- package/examples/halo-explainer/stills/t000.00.png +0 -0
- package/examples/halo-explainer/stills/t006.00.png +0 -0
- package/examples/halo-explainer/stills/t012.00.png +0 -0
- package/examples/halo-explainer/stills/t018.00.png +0 -0
- package/examples/halo-explainer/stills/t024.00.png +0 -0
- package/examples/halo-explainer/stills/t030.00.png +0 -0
- package/examples/install-loop/stills/contact-sheet.jpg +0 -0
- package/examples/install-loop/stills/t000.00.png +0 -0
- package/examples/install-loop/stills/t001.00.png +0 -0
- package/examples/install-loop/stills/t002.00.png +0 -0
- package/examples/install-loop/stills/t003.00.png +0 -0
- package/examples/install-loop/stills/t004.00.png +0 -0
- package/examples/install-loop/stills/t005.00.png +0 -0
- package/examples/orbit-keynote/stills/contact-sheet.jpg +0 -0
- package/examples/orbit-keynote/stills/t000.00.png +0 -0
- package/examples/orbit-keynote/stills/t006.50.png +0 -0
- package/examples/orbit-keynote/stills/t013.00.png +0 -0
- package/examples/orbit-keynote/stills/t019.50.png +0 -0
- package/examples/orbit-keynote/stills/t026.00.png +0 -0
- package/examples/orbit-keynote/stills/t032.50.png +0 -0
- package/examples/signal/stills/contact-sheet.jpg +0 -0
- package/examples/signal/stills/t000.00.png +0 -0
- package/examples/signal/stills/t001.67.png +0 -0
- package/examples/signal/stills/t003.33.png +0 -0
- package/examples/signal/stills/t005.00.png +0 -0
- package/examples/signal/stills/t006.67.png +0 -0
- package/examples/signal/stills/t008.33.png +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,84 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the `paper-motion` npm package. Dates are release dates on npm.
|
|
4
4
|
|
|
5
|
+
## 0.5.0 · 2026-11-13
|
|
6
|
+
|
|
7
|
+
**Studio v2: one page, four steps, an honest agent.** The user makes a piece in the studio alone (What you want to
|
|
8
|
+
say, How it looks, Your video, Export); the agent listens in the background, and its visual choices come into the
|
|
9
|
+
studio instead of links. Five phases, one end-to-end check (`scripts/check-studio.mjs`, 53 checks).
|
|
10
|
+
|
|
11
|
+
Studio v2, phase 1 ([spec](../../docs/spec-studio-v2.md), [plan](../../docs/plan-studio-v2.md)): an honest connection with the agent.
|
|
12
|
+
|
|
13
|
+
- **`piece listen <piece>`**: the agent keeps it running in the background between turns. It returns as soon as the
|
|
14
|
+
user does something that needs the agent (a message, an answer, an approval, a choice), printing it as lines and
|
|
15
|
+
marking them read, or exits 3 after 9 minutes with nothing. The user no longer has to go to the chat to say
|
|
16
|
+
"continue".
|
|
17
|
+
- **The studio only says what it can check about the agent:** listening (a command of the agent is waiting on the
|
|
18
|
+
piece, and its process is alive), working on something (said with `piece working … --on "…"`, for up to 20
|
|
19
|
+
minutes), asking you, or not connected. "Your agent waits" and "tell your agent continue" are gone. A command
|
|
20
|
+
that was killed stops showing as listening at once.
|
|
21
|
+
- **Questions are answered in the studio.** `piece ask` shows the question with an answer box; the answer reaches
|
|
22
|
+
`piece listen` as an `answered "…": "…"` line. **Answered in the chat** closes it.
|
|
23
|
+
- **Ask your agent is a thread:** each message shows Sent, then Read with the time; `piece say <piece> "…"` puts the
|
|
24
|
+
agent's reply there. A message no longer moves the stage to "changes".
|
|
25
|
+
- `scripts/check-studio.mjs` drives a fresh project end to end (headless Chrome for the user, real `pamo` commands
|
|
26
|
+
for the agent).
|
|
27
|
+
|
|
28
|
+
Studio v2, phase 2: one page, four steps, and How it looks.
|
|
29
|
+
|
|
30
|
+
- **A new stage, `look`**, between `inventory` and `video` (`STAGES`: brief, inventory, look, video, final). A
|
|
31
|
+
project whose brand already settles colours, type and a background starts with it done; older pieces get it as
|
|
32
|
+
approved.
|
|
33
|
+
- **The stepper:** What you want to say · How it looks · Your video · Export. Steps ahead stay locked until reached;
|
|
34
|
+
earlier ones reopen. Style moved into How it looks.
|
|
35
|
+
- **How it looks:** colours, type, background and movement (pace, and how words arrive), each a still card. Pointing
|
|
36
|
+
at one plays it in a single live preview of the user's own first sentence (`line-in`, with the look laid over the
|
|
37
|
+
brand only for that preview: `?embed&look=…`, read by `runtime/brand.js`). Choosing writes `brand/`: the palette
|
|
38
|
+
(the brand's library backgrounds take its colours again), the font pair, a colour by hand, the brand's main
|
|
39
|
+
background (added from the library in its colours when needed), pace and the default entrance
|
|
40
|
+
(`brand/motion.json` `entrance`, which `line-in` uses when a part sets none). **Make my video** builds the first
|
|
41
|
+
version, which used to happen on approving the content.
|
|
42
|
+
- **A still of every background** (`apps/web/public/previews/<id>.jpg`, from `scripts/render-backgrounds.mjs`), so
|
|
43
|
+
cards never run a shader.
|
|
44
|
+
|
|
45
|
+
Studio v2, phase 3: the agent's choices come into the studio ([ADR 0005](../../docs/adr/0005-the-studio-replaces-board-links-during-a-piece.md)).
|
|
46
|
+
|
|
47
|
+
- With a piece open and its studio running, **`pamo board --url …` puts the options into the studio** instead of a
|
|
48
|
+
link, on the step they decide: palettes, font pairs, backgrounds and moves in How it looks (kits as a whole look
|
|
49
|
+
at its top), transitions, structures and blocks in Your video. The step shows a dot; the options come first in
|
|
50
|
+
their section, marked **Suggested by your agent**, with the agent's question and notes, and play in the preview
|
|
51
|
+
when pointed at. The command waits (the studio shows the agent listening) and prints the same reply line, built
|
|
52
|
+
with `replyLine`. **None of these** sends what the user would rather see. `--link` makes a link anyway.
|
|
53
|
+
- A palette, font pair or background picked there is applied to `brand/` at once, like any card; moves,
|
|
54
|
+
transitions and structures go to the agent as the line.
|
|
55
|
+
- `board --await c1` waits again for a studio choice, `board --forget c1` takes it out.
|
|
56
|
+
|
|
57
|
+
Studio v2, phase 4: Your video as scenes.
|
|
58
|
+
|
|
59
|
+
- **A list of scenes instead of the strip:** each with its number, a still of itself, what it says and how long it
|
|
60
|
+
plays; drag to reorder; **Add a scene** at the end asks what it is for.
|
|
61
|
+
- **Stills of the real scenes** (the user's words, brand and settings), rendered by the studio one at a time after a
|
|
62
|
+
scene changes (`pieces/<piece>/.stills/`), never while a final renders; until then the library's still shows.
|
|
63
|
+
- **Five tabs:** Text, Animation (with the block's own settings), Background, Speed (length is speed) and Next scene
|
|
64
|
+
(the transition, chosen by watching, or the same between every scene).
|
|
65
|
+
- **Nothing rebuilds by itself.** Something new kept in What you want to say offers **Add it to the video**; approving
|
|
66
|
+
the content again never takes a built piece back to How it looks.
|
|
67
|
+
- **The playing scene no longer restarts when the page redraws** (a still arriving, the agent's state changing): it is
|
|
68
|
+
moved without reloading (`moveBefore`, where the browser has it), and a still only updates its thumbnail.
|
|
69
|
+
|
|
70
|
+
Studio v2, phase 5: light on the machine.
|
|
71
|
+
|
|
72
|
+
- An embedded preview that ended stops its frame loop (a click starts it again): an idle studio draws nothing. The
|
|
73
|
+
check measures it: 0 shader draws and 0 animation frames for 5 s after a scene ends, and at most one live preview
|
|
74
|
+
on the page.
|
|
75
|
+
- **Fixed for npm installs:** the site's `registry.json` lists params by name only, so an installed studio had no
|
|
76
|
+
block settings or entrances to offer (since 0.4) and, now, no palettes' colours, font pairs or backgrounds. It
|
|
77
|
+
fetches the full resource of those (`/r/<id>.json`) when it starts. `scripts/check-studio.mjs` runs against an
|
|
78
|
+
installed package with `PAMO_BIN=…`.
|
|
79
|
+
- The site's making-of, hero and docs page tell the four steps, the five stages behind them, `piece listen` and the
|
|
80
|
+
choices in the studio.
|
|
81
|
+
|
|
82
|
+
|
|
5
83
|
## 0.4.1 · 2026-11-12
|
|
6
84
|
|
|
7
85
|
- **The studio's live previews are much lighter.** A scene opened with `?embed` draws its shaders at the size it is
|
package/bin/pamo.mjs
CHANGED
|
@@ -364,7 +364,7 @@ async function main() {
|
|
|
364
364
|
const { parseSet, setArgs } = await import('../lib/customize.mjs');
|
|
365
365
|
const reg = resolveRegistry(args.registry), { items } = await getIndex(reg), index = Object.fromEntries(items.map((i) => [i.id, i]));
|
|
366
366
|
const opts = process.argv.flatMap((a, i, all) => (a === '--option' ? [all[i + 1]] : a.startsWith('--option=') ? [a.slice(9)] : [])).filter(Boolean);
|
|
367
|
-
if (!opts.length || !args.title || args.help) { console.error('Usage: board --url --title "…" [--ask "…"] [--reply name] [--look palette[:key=value,…],font | --look brand] --option id[:key=value,…] [--note "why"] … [--kit] [--choose 1-3] [--wait minutes | --no-wait] [--long] [--site url]\n board --await <id|link> [--wait minutes] (wait again for a board already shared)\n board --forget <id>\n On a recipe board an option may be a structure of your own: --option "@Title | 18 | Hook 0-2 [word-swap] one line; Proof 2-12 [browser-frame] three shots; Close 12-18 [svg-logo] the action" (a [block-id] makes the board play that clip for the beat).\n Prints a live board link: short (/b/<id>, stored on the site for 90 days) when the site answers, long (the board in the URL fragment) otherwise or with --long. Values after the colon are your adjustments; the reply line carries every non-default value.\n With a short link the command keeps running until the user chooses in the browser (10 min; --wait 3 changes it, in minutes; --no-wait returns at once) and prints the reply line, the same one they could paste. If your shell caps how long a command runs, share with --no-wait and then wait in short steps: board --await <id> --wait 2, again until it prints the line (exit code 3 means not yet). Values take lists and several keys: --option graphite:accent=#B8F03C,ramp=["#0A0A0B","#B8F03C"] (quote it for the shell); --look brand uses the palette, colors and font pair this project installed. --choose lets them pick several options (a count, or min-max). --forget deletes a stored board early.'); process.exit(args.help ? 0 : 1); }
|
|
367
|
+
if (!opts.length || !args.title || args.help) { console.error('Usage: board --url --title "…" [--ask "…"] [--reply name] [--look palette[:key=value,…],font | --look brand] --option id[:key=value,…] [--note "why"] … [--kit] [--choose 1-3] [--wait minutes | --no-wait] [--long] [--site url] [--link] (with a piece open and its studio running the options go into the studio, not a link; --link makes a link anyway)\n board --await <id|link> [--wait minutes] (wait again for a board already shared)\n board --forget <id>\n On a recipe board an option may be a structure of your own: --option "@Title | 18 | Hook 0-2 [word-swap] one line; Proof 2-12 [browser-frame] three shots; Close 12-18 [svg-logo] the action" (a [block-id] makes the board play that clip for the beat).\n Prints a live board link: short (/b/<id>, stored on the site for 90 days) when the site answers, long (the board in the URL fragment) otherwise or with --long. Values after the colon are your adjustments; the reply line carries every non-default value.\n With a short link the command keeps running until the user chooses in the browser (10 min; --wait 3 changes it, in minutes; --no-wait returns at once) and prints the reply line, the same one they could paste. If your shell caps how long a command runs, share with --no-wait and then wait in short steps: board --await <id> --wait 2, again until it prints the line (exit code 3 means not yet). Values take lists and several keys: --option graphite:accent=#B8F03C,ramp=["#0A0A0B","#B8F03C"] (quote it for the shell); --look brand uses the palette, colors and font pair this project installed. --choose lets them pick several options (a count, or min-max). --forget deletes a stored board early.'); process.exit(args.help ? 0 : 1); }
|
|
368
368
|
const LABELS = ['A', 'B', 'C', 'D'];
|
|
369
369
|
// commas only separate at the top level (not inside [..], {..} or quotes), and only before a new "key=":
|
|
370
370
|
// so ramp=["#000","#111"] and multi-value colors survive
|
|
@@ -420,6 +420,31 @@ async function main() {
|
|
|
420
420
|
} else if (typeof args.look === 'string') for (const spec of splitLook(args.look)) { const o = await parseOne(spec); const t = index[o.id]?.type; if (t === 'palette') look.palette = o; else if (t === 'font-pair') look.font = o; else throw new Error(`--look takes a palette and a font pair (or "brand"), not "${o.id}"`); }
|
|
421
421
|
const choose = args.choose === undefined ? undefined : /^\d+-\d+$/.test(String(args.choose)) ? String(args.choose).split('-').map(Number) : Number(args.choose);
|
|
422
422
|
const board = { v: 1, kind: args.kit ? 'kit' : 'resource', title: String(args.title), ...(args.ask ? { ask: String(args.ask) } : {}), ...(args.reply ? { reply: String(args.reply) } : {}), ...(Object.keys(look).length ? { look } : {}), ...(choose !== undefined ? { choose } : {}), options };
|
|
423
|
+
// a piece open with its studio running: the options go into the studio, on the step they decide, not into a
|
|
424
|
+
// link; the command waits there and prints the same reply line (ADR 0005). --link makes a site link anyway.
|
|
425
|
+
if (!args.link) {
|
|
426
|
+
const PCs = await import('../lib/piece.mjs');
|
|
427
|
+
const pdirS = PCs.activePiece(process.cwd(), typeof args.piece === 'string' ? args.piece : undefined);
|
|
428
|
+
const st = pdirS ? await (await import('../lib/studio.mjs')).findStudio(process.cwd()) : null;
|
|
429
|
+
if (st) {
|
|
430
|
+
const { validateBoard } = await import('../lib/live-board.js');
|
|
431
|
+
const errs = validateBoard(board, index); if (errs.length) { console.error(`✗ ${errs.join('; ')}`); process.exit(1); }
|
|
432
|
+
const type = board.kind === 'kit' ? 'kit' : board.options[0].structure ? 'structure' : index[board.options[0].id]?.type;
|
|
433
|
+
const PLACE = { palette: ['look', 'colours', 'How it looks · Colours'], 'font-pair': ['look', 'type', 'How it looks · Type'], background: ['look', 'background', 'How it looks · Background'],
|
|
434
|
+
text: ['look', 'movement', 'How it looks · Movement'], component: ['look', 'movement', 'How it looks · Movement'], camera: ['look', 'movement', 'How it looks · Movement'], kit: ['look', 'kit', 'How it looks'],
|
|
435
|
+
transition: ['video', 'joins', 'Your video'], structure: ['video', 'structure', 'Your video'], recipe: ['video', 'structure', 'Your video'], block: ['video', 'scenes', 'Your video'], shot: ['video', 'scenes', 'Your video'] };
|
|
436
|
+
const [step, section, label] = PLACE[type] ?? ['video', 'other', 'Your video'];
|
|
437
|
+
const cid = PCs.addChoice(pdirS, { board, where: { step, section, type } });
|
|
438
|
+
console.log(`in the studio, on ${label}: ${st.url}?p=${encodeURIComponent(path.basename(pdirS))} (choice ${cid}; no link to share)`);
|
|
439
|
+
if (args['no-wait']) { console.error(`the user chooses in the studio; wait for it with: board --await ${cid}`); break; }
|
|
440
|
+
const minutes = Number.isFinite(Number(args.wait)) && args.wait !== undefined && args.wait !== true ? Number(args.wait) : 10;
|
|
441
|
+
console.error(`waiting up to ${minutes} min for their choice in the studio…`);
|
|
442
|
+
const line = await PCs.awaitChoiceIn(pdirS, cid, { timeout: minutes * 60 * 1000 });
|
|
443
|
+
if (!line) { console.error(`no choice yet: wait again with board --await ${cid}`); process.exit(3); }
|
|
444
|
+
console.log(line);
|
|
445
|
+
break;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
423
448
|
const site = typeof args.site === 'string' ? args.site : SITE;
|
|
424
449
|
let frag, short = null;
|
|
425
450
|
try { frag = await encodeBoard(board, index); } catch (e) { console.error(e.message); process.exit(1); }
|
|
@@ -443,6 +468,22 @@ async function main() {
|
|
|
443
468
|
} else if (!short && wait) console.error('(no short link, so no waiting: the user will paste the line from the board)');
|
|
444
469
|
break;
|
|
445
470
|
}
|
|
471
|
+
if (args.await && /^c\d+$/.test(String(args.await))) { // a choice in the studio
|
|
472
|
+
const PCs = await import('../lib/piece.mjs'), pdirS = PCs.activePiece(process.cwd(), typeof args.piece === 'string' ? args.piece : undefined);
|
|
473
|
+
if (!pdirS) { console.error('no open piece here (pass --piece)'); process.exit(1); }
|
|
474
|
+
const minutes = Number.isFinite(Number(args.wait)) && args.wait !== undefined && args.wait !== true ? Number(args.wait) : 10;
|
|
475
|
+
console.error(`waiting up to ${minutes} min for their choice in the studio…`);
|
|
476
|
+
const line = await PCs.awaitChoiceIn(pdirS, String(args.await), { timeout: minutes * 60 * 1000 });
|
|
477
|
+
if (!line) { console.error('no choice yet: run it again'); process.exit(3); }
|
|
478
|
+
console.log(line);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
if (args.forget && /^c\d+$/.test(String(args.forget))) {
|
|
482
|
+
const PCs = await import('../lib/piece.mjs'), pdirS = PCs.activePiece(process.cwd(), typeof args.piece === 'string' ? args.piece : undefined);
|
|
483
|
+
if (!pdirS) { console.error('no open piece here (pass --piece)'); process.exit(1); }
|
|
484
|
+
PCs.forgetChoice(pdirS, String(args.forget)); console.log(`took choice ${args.forget} out of the studio`);
|
|
485
|
+
break;
|
|
486
|
+
}
|
|
446
487
|
if (args.await) {
|
|
447
488
|
// wait (again) for the choice on a board already shared: for shells that cap how long one command may run
|
|
448
489
|
const { awaitChoice } = await import('../lib/live-board.js');
|
|
@@ -657,7 +698,7 @@ async function main() {
|
|
|
657
698
|
|
|
658
699
|
case 'piece': {
|
|
659
700
|
const sub = args._[1], piece = args._[2];
|
|
660
|
-
if (!['init', 'review', 'working', 'status', 'ask'].includes(sub) || !piece || args.help) { console.error('Usage: piece init <piece> [--title "…"] [--what …] [--who …] [--format …] [--tone …] [--action …] [--no-studio] [--force]\n piece review <piece> <stage> [--wait 30 (minutes)] [--no-wait] [--no-open] [--no-studio] [--skip]\n piece working <piece> <stage
|
|
701
|
+
if (!['init', 'review', 'working', 'status', 'ask', 'listen', 'say'].includes(sub) || !piece || args.help) { console.error('Usage: piece init <piece> [--title "…"] [--what …] [--who …] [--format …] [--tone …] [--action …] [--no-studio] [--force]\n piece review <piece> <stage> [--wait 30 (minutes)] [--no-wait] [--no-open] [--no-studio] [--skip]\n piece listen <piece> [--wait 9 (minutes)]\n piece working <piece> <stage> [--on "what you are doing, in plain words"]\n piece say <piece> "a short reply the user reads in the studio"\n piece ask <piece> "the question" | --done\n piece status <piece>\n listen: run it in the background between your turns; it returns as soon as the user does something that needs you (a message, an answer, an approval, a choice), printing it as lines, or after --wait minutes with nothing (exit 3: start it again). While it runs the studio says you are listening.\n ask: run it right before you ask the user anything in the chat, so the studio sends them there (the next review or working clears it, or --done). --return <url> on any of these gives the studio a link back to you when you run in a browser.\n Stages: brief, inventory (what the video can show), video (they edit it in the studio), final. review hands a stage to the user in the studio and prints what they did, one line each; it refuses while an earlier stage is not approved (--skip moves on without it, only when the user agreed). --no-wait returns at once with what is unread; --no-open does not open their browser; --no-studio runs without the studio.'); process.exit(args.help ? 0 : 1); }
|
|
661
702
|
const P = await import('../lib/piece.mjs');
|
|
662
703
|
const { pieceDir } = await import('../lib/chain.mjs');
|
|
663
704
|
const dir = pieceDir(piece), studioLib = await import('../lib/studio.mjs'), q = `?p=${encodeURIComponent(path.basename(dir))}`;
|
|
@@ -689,15 +730,31 @@ async function main() {
|
|
|
689
730
|
await host();
|
|
690
731
|
if (args.done) { P.clearAsk(dir); console.log('✓ cleared: the studio no longer sends the user to the chat'); break; }
|
|
691
732
|
const question = typeof args._[3] === 'string' ? args._[3].trim() : '';
|
|
692
|
-
if (!question) { console.error('piece ask: give the question in one line
|
|
733
|
+
if (!question) { console.error('piece ask: give the question in one line, or --done'); process.exit(1); }
|
|
693
734
|
P.setAsk(dir, question);
|
|
694
|
-
console.log(`✓ the studio
|
|
735
|
+
console.log(`✓ the studio shows your question with a box to answer it; their answer reaches piece listen ${piece} as an "answered" line. Ask it in the chat too. It closes when they answer, or with the next piece review or piece working (or piece ask ${piece} --done).`);
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
if (sub === 'listen') {
|
|
739
|
+
if (!P.readPiece(dir)) { console.error(`no piece.json in ${path.relative(process.cwd(), dir)}`); process.exit(1); }
|
|
740
|
+
await host();
|
|
741
|
+
const minutes = Number.isFinite(Number(args.wait)) && args.wait !== undefined && args.wait !== true ? Number(args.wait) : 9;
|
|
742
|
+
const r = await P.listenFor(dir, { timeout: minutes * 60 * 1000 });
|
|
743
|
+
for (const l of r.lines) console.log(l);
|
|
744
|
+
if (!r.done) { console.log(`(nothing from the user in ${minutes} min: run piece listen ${piece} again)`); process.exit(3); }
|
|
745
|
+
break;
|
|
746
|
+
}
|
|
747
|
+
if (sub === 'say') {
|
|
748
|
+
if (!P.readPiece(dir)) { console.error(`no piece.json in ${path.relative(process.cwd(), dir)}`); process.exit(1); }
|
|
749
|
+
const text = typeof args._[3] === 'string' ? args._[3].trim() : '';
|
|
750
|
+
if (!text) { console.error('piece say: give the reply in one or two plain sentences'); process.exit(1); }
|
|
751
|
+
P.addNote(dir, text); console.log('✓ the user sees it in the studio, under Ask your agent');
|
|
695
752
|
break;
|
|
696
753
|
}
|
|
697
754
|
const stage = args._[3];
|
|
698
755
|
await host();
|
|
699
756
|
if (!P.STAGES.includes(stage)) { console.error(`piece ${sub}: give a stage (${P.STAGES.join(', ')})`); process.exit(1); }
|
|
700
|
-
if (sub === 'working') { P.updatePiece(dir, (p) => {
|
|
757
|
+
if (sub === 'working') { P.updatePiece(dir, (p) => { p.stage = stage; p.stages[stage] = { ...p.stages[stage], status: 'working', since: new Date().toISOString() }; }); P.setWorking(dir, typeof args.on === 'string' ? args.on : null); console.log(`✓ ${stage}: working${typeof args.on === 'string' ? ` on ${args.on}` : ''} (the studio shows it until you listen or review, or for 20 minutes)`); break; }
|
|
701
758
|
// review: hand the stage over and wait for the user's decision in the studio
|
|
702
759
|
if (stage === 'inventory') {
|
|
703
760
|
// the user is asked whether each thing is true and worth showing: they cannot judge half an item
|
package/lib/edit.mjs
CHANGED
|
@@ -320,19 +320,47 @@ export async function ensureLook(cwd, name) {
|
|
|
320
320
|
return installed;
|
|
321
321
|
}
|
|
322
322
|
|
|
323
|
+
// the brand's backgrounds that come from the library take its colours again (after the palette or a colour changed)
|
|
324
|
+
async function recolorBackgrounds(cwd) {
|
|
325
|
+
const setFile = path.join(cwd, 'brand/shader-set/set.json'), set = readJson(setFile, {});
|
|
326
|
+
const { add, getIndex } = await import('./registry.mjs');
|
|
327
|
+
const index = new Set(((await getIndex().catch(() => ({ items: [] }))).items ?? []).filter((x) => x.type === 'background').map((x) => x.id));
|
|
328
|
+
const ids = (set.entries ?? []).filter((e) => e.shader && !e.image && index.has(e.id)).map((e) => e.id);
|
|
329
|
+
if (!ids.length) return;
|
|
330
|
+
const order = (set.entries ?? []).map((e) => e.id);
|
|
331
|
+
await add(ids, { cwd, palette: 'brand', force: true });
|
|
332
|
+
const after = readJson(setFile, {}); // keep the order the user had: the first is the brand's background
|
|
333
|
+
after.entries = [...(after.entries ?? [])].sort((x, y) => (order.indexOf(x.id) + 1 || 1e9) - (order.indexOf(y.id) + 1 || 1e9));
|
|
334
|
+
writeJson(setFile, after);
|
|
335
|
+
}
|
|
336
|
+
|
|
323
337
|
export async function editStyle(cwd, dir, a) {
|
|
324
|
-
const tokens = path.join(cwd, 'brand/tokens.json'), motion = path.join(cwd, 'brand/motion.json');
|
|
325
|
-
snapshot(cwd, dir, [tokens, motion, path.join(cwd, 'brand/fonts.css')], 'the style');
|
|
338
|
+
const tokens = path.join(cwd, 'brand/tokens.json'), motion = path.join(cwd, 'brand/motion.json'), setFile = path.join(cwd, 'brand/shader-set/set.json');
|
|
339
|
+
snapshot(cwd, dir, [tokens, motion, path.join(cwd, 'brand/fonts.css'), setFile], 'the style');
|
|
326
340
|
if (a.type === 'palette' || a.type === 'font') {
|
|
327
341
|
const { add } = await import('./registry.mjs');
|
|
328
342
|
await add([a.value], { cwd, force: true });
|
|
343
|
+
if (a.type === 'palette') await recolorBackgrounds(cwd);
|
|
329
344
|
return { say: `${a.type === 'palette' ? 'Colours' : 'Type'}: ${a.value}` };
|
|
330
345
|
}
|
|
331
346
|
if (a.type === 'color') {
|
|
332
347
|
if (!/^#[0-9a-f]{6}$/i.test(a.value) || !['ground', 'figure', 'accent'].includes(a.key)) throw new Error('bad colour');
|
|
333
|
-
const t = readJson(tokens, {}); t.colors ??= {}; t.colors[a.key] = { ...(t.colors[a.key] ?? {}), hex: a.value.toUpperCase() }; writeJson(tokens, t);
|
|
348
|
+
const t = readJson(tokens, {}); t.colors ??= {}; t.colors[a.key] = { ...(t.colors[a.key] ?? {}), hex: a.value.toUpperCase() }; delete t.ramp; writeJson(tokens, t);
|
|
349
|
+
await recolorBackgrounds(cwd);
|
|
334
350
|
return { say: `${{ ground: 'Background', figure: 'Text', accent: 'Accent' }[a.key]} colour ${a.value}` };
|
|
335
351
|
}
|
|
352
|
+
if (a.type === 'background') { // the brand's background: the first entry of its set, added from the library in its colours
|
|
353
|
+
let set = readJson(setFile, { entries: [] });
|
|
354
|
+
if (!(set.entries ?? []).some((e) => e.id === a.value)) { const { add } = await import('./registry.mjs'); await add([a.value], { cwd, palette: 'brand' }); set = readJson(setFile, { entries: [] }); }
|
|
355
|
+
const i = set.entries.findIndex((e) => e.id === a.value); if (i < 0) throw new Error('no such background');
|
|
356
|
+
set.entries.unshift(...set.entries.splice(i, 1)); writeJson(setFile, set);
|
|
357
|
+
return { say: `Background: ${a.value}` };
|
|
358
|
+
}
|
|
359
|
+
if (a.type === 'entrance') { // how words arrive, unless a part says otherwise
|
|
360
|
+
if (!/^[a-z-]+$/.test(a.value)) throw new Error('bad entrance');
|
|
361
|
+
const m = readJson(motion, {}); m.entrance = a.value; writeJson(motion, m);
|
|
362
|
+
return { say: `Words arrive: ${a.value}` };
|
|
363
|
+
}
|
|
336
364
|
if (a.type === 'pace') {
|
|
337
365
|
if (!['calm', 'medium', 'fast'].includes(a.value)) throw new Error('bad pace');
|
|
338
366
|
const m = readJson(motion, {}); m.pace = a.value; writeJson(motion, m);
|
package/lib/piece.mjs
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
// A piece's progress: pieces/<piece>/piece.json, shared by the agent (CLI) and the user (pamo studio).
|
|
2
2
|
//
|
|
3
|
-
// stage the stage the piece is at: brief → inventory → video → final. The user sees
|
|
4
|
-
//
|
|
3
|
+
// stage the stage the piece is at: brief → inventory → look → video → final. The user sees four steps (What
|
|
4
|
+
// you want to say: the brief and the inventory; How it looks; Your video; Export).
|
|
5
5
|
// stages each stage's status: pending · working (the agent's turn) · review (the user's turn) · changes · approved
|
|
6
6
|
// brief what, who, format, tone, action: written by the agent, editable by the user
|
|
7
7
|
// parts per part (by its folder in chain.json): status (queued · rendering · review · changes · approved), note
|
|
8
|
-
// events what the user did in the studio, in order; `seen` is the last one the agent has read
|
|
9
|
-
//
|
|
8
|
+
// events what the user did in the studio, in order; `seen` is the last one the agent has read, and each
|
|
9
|
+
// event the agent read carries `readAt`
|
|
10
|
+
// ask a question the agent asked and is waiting on; the user answers it in the studio (an `answer` event)
|
|
11
|
+
// working what the agent said it is doing (`piece working --on`) and since when; it lapses after 20 minutes
|
|
12
|
+
// notes the agent's short replies to the user, shown in the studio's thread (`piece say`)
|
|
13
|
+
// choices options the agent put into the studio (`pamo board` while a piece is open), where they show, and
|
|
14
|
+
// the reply line once the user chose (ADR 0005)
|
|
10
15
|
// agent the app or page the agent runs in, so the studio can take the user back to it
|
|
11
16
|
//
|
|
12
17
|
// chain.json stays the source of what plays (order, seconds, joins); piece.json only tracks decisions.
|
|
@@ -14,7 +19,7 @@ import fs from 'node:fs';
|
|
|
14
19
|
import path from 'node:path';
|
|
15
20
|
import { pieceDir } from './chain.mjs';
|
|
16
21
|
|
|
17
|
-
export const STAGES = ['brief', 'inventory', 'video', 'final'];
|
|
22
|
+
export const STAGES = ['brief', 'inventory', 'look', 'video', 'final'];
|
|
18
23
|
// the stages a piece had before Your video took them over: a piece at one of them is at `video` now
|
|
19
24
|
const OLD_VIDEO = ['look', 'structure', 'parts', 'assembly'];
|
|
20
25
|
export const STAGE_STATUS = ['pending', 'working', 'review', 'changes', 'approved'];
|
|
@@ -39,6 +44,13 @@ function upgrade(p) {
|
|
|
39
44
|
return p;
|
|
40
45
|
}
|
|
41
46
|
|
|
47
|
+
/** A project whose brand already settles colours, type and a background: How it looks starts done. */
|
|
48
|
+
export function brandSettled(cwd) {
|
|
49
|
+
const read = (f) => { try { return JSON.parse(fs.readFileSync(path.join(cwd, f), 'utf8')); } catch { return {}; } };
|
|
50
|
+
const t = read('brand/tokens.json'), set = read('brand/shader-set/set.json');
|
|
51
|
+
return !!(t.colors?.ground && t.colors?.figure && t.fonts?.display && (Array.isArray(set) ? set : set.entries ?? []).some((e) => e.shader && !e.image));
|
|
52
|
+
}
|
|
53
|
+
|
|
42
54
|
/** Writes atomically (the studio and the CLI both write it). */
|
|
43
55
|
export function writePiece(dir, p) {
|
|
44
56
|
fs.mkdirSync(dir, { recursive: true });
|
|
@@ -67,7 +79,7 @@ export function initPiece({ cwd = process.cwd(), piece, title, brief = {}, force
|
|
|
67
79
|
return writePiece(dir, {
|
|
68
80
|
v: 1, piece: path.basename(dir), title: title ?? path.basename(dir), brand: tokens.brand ?? null,
|
|
69
81
|
stage: 'brief',
|
|
70
|
-
stages: Object.fromEntries(STAGES.map((s, i) => [s, { status: i ? 'pending' : 'working' }])),
|
|
82
|
+
stages: Object.fromEntries(STAGES.map((s, i) => [s, s === 'look' && brandSettled(cwd) ? { status: 'approved', brand: true } : { status: i ? 'pending' : 'working' }])),
|
|
71
83
|
brief: Object.fromEntries(BRIEF_KEYS.map((k) => [k, brief[k] ?? ''])),
|
|
72
84
|
parts: {}, events: [], seen: 0, created: now(), updated: now(),
|
|
73
85
|
});
|
|
@@ -75,12 +87,14 @@ export function initPiece({ cwd = process.cwd(), piece, title, brief = {}, force
|
|
|
75
87
|
|
|
76
88
|
/** The line the agent reads for an event (same family as the board reply). */
|
|
77
89
|
export function eventLine(p, e) {
|
|
78
|
-
const who = e.part ? `part ${e.part}` : e.stage;
|
|
90
|
+
const who = e.part ? `part ${e.part}` : e.stage ?? '';
|
|
79
91
|
const what = e.kind === 'approve' ? 'approved'
|
|
80
92
|
: e.kind === 'changes' ? `changes: "${e.text}"`
|
|
81
93
|
: e.kind === 'order' ? `order ${e.text}`
|
|
82
94
|
: e.kind === 'reopen' ? 'reopened'
|
|
83
95
|
: e.kind === 'failed' ? `could not join the parts: "${e.text}"`
|
|
96
|
+
: e.kind === 'answer' ? `answered${e.q ? ` "${e.q}"` : ''}: "${e.text}"`
|
|
97
|
+
: e.kind === 'choice' ? `chose${e.title ? ` on "${e.title}"` : ''}: ${e.text}`
|
|
84
98
|
: e.say ? e.say : `${e.key}="${e.text}"${e.rerendered ? ' (re-rendered)' : ''}`;
|
|
85
99
|
// the studio joins the draft again by itself after an edit by hand; the full-quality render stays the agent's
|
|
86
100
|
const tail = `${e.rejoined ? ' (the studio is joining the draft again)' : ''}${e.rejoin ? ' · join again: pamo chain --draft' : ''}${e.refinal ? ` · the final is out of date: render it again (pamo chain ${p.piece} --motion-blur 5), then review final` : ''}`;
|
|
@@ -105,6 +119,7 @@ export function toReview(dir, stage, { skip = false } = {}) {
|
|
|
105
119
|
if (open.length && !skip) throw new Error(`${open.join(', ')} not approved yet: review ${open.length > 1 ? 'them' : 'it'} first (piece review ${p.piece} ${open[0]}), or pass --skip when the user agreed to move on without ${open.length > 1 ? 'them' : 'it'}`);
|
|
106
120
|
for (const s of open) p.stages[s] = { ...p.stages[s], status: 'approved' };
|
|
107
121
|
delete p.ask; // handing a stage over means the question in the chat is settled
|
|
122
|
+
delete p.working;
|
|
108
123
|
p.stage = stage;
|
|
109
124
|
p.stages[stage] = { status: 'review', since: now() };
|
|
110
125
|
});
|
|
@@ -116,10 +131,55 @@ export const earlierOpen = (p, stage) => STAGES.slice(0, STAGES.indexOf(stage)).
|
|
|
116
131
|
/** Events the agent hasn't read; marks them read when `mark`. */
|
|
117
132
|
export function unseen(dir, { mark = true } = {}) {
|
|
118
133
|
let out = [];
|
|
119
|
-
const p = updatePiece(dir, (q) => {
|
|
134
|
+
const p = updatePiece(dir, (q) => {
|
|
135
|
+
out = q.events.filter((e) => e.n > (q.seen ?? 0));
|
|
136
|
+
if (mark && out.length) { const at = now(); for (const e of out) e.readAt = at; q.seen = out.at(-1).n; }
|
|
137
|
+
});
|
|
120
138
|
return { piece: p, events: out };
|
|
121
139
|
}
|
|
122
140
|
|
|
141
|
+
// what the user does that needs the agent: something asked, answered, approved, reopened, chosen, or a join that failed
|
|
142
|
+
const needsAgent = (e) => ['changes', 'answer', 'reopen', 'choice', 'failed'].includes(e.kind) || (e.kind === 'approve' && !e.part) || e.rejoin || e.refinal;
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Listens on the piece between the agent's turns (`pamo piece listen`): returns as soon as the user does something
|
|
146
|
+
* that needs the agent, with every unread event as lines (and marks them read), or after `timeout` with nothing.
|
|
147
|
+
* While it listens the studio says so; it is the agent's way to hear the studio without the user going to the chat.
|
|
148
|
+
*/
|
|
149
|
+
export async function listenFor(dir, { timeout = 9 * 60 * 1000, every = 1000, signal } = {}) {
|
|
150
|
+
const t0 = Date.now();
|
|
151
|
+
let last = 0;
|
|
152
|
+
updatePiece(dir, (p) => { delete p.working; }); // listening means it is not doing anything else
|
|
153
|
+
// interrupted (the agent cancels it, Ctrl+C): stop saying it listens right away
|
|
154
|
+
const stop = () => { unbeat(dir); process.exit(130); };
|
|
155
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
156
|
+
try {
|
|
157
|
+
for (;;) {
|
|
158
|
+
if (Date.now() - last > 3000) { beat(dir, { waiting: 'listen' }); last = Date.now(); }
|
|
159
|
+
const p = readPiece(dir);
|
|
160
|
+
const fresh = p.events.filter((e) => e.n > (p.seen ?? 0));
|
|
161
|
+
if (fresh.some(needsAgent)) { const r = unseen(dir); return { done: true, piece: r.piece, events: r.events, lines: r.events.map((e) => eventLine(r.piece, e)) }; }
|
|
162
|
+
if (Date.now() - t0 > timeout || signal?.aborted) return { done: false, piece: p, events: [], lines: [] };
|
|
163
|
+
await new Promise((res) => setTimeout(res, every));
|
|
164
|
+
}
|
|
165
|
+
} finally { unbeat(dir); process.off('SIGINT', stop); process.off('SIGTERM', stop); }
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const WORKING_FOR = 20 * 60 * 1000;
|
|
169
|
+
/** The agent says what it is doing; the studio shows it, with how long, until it listens, reviews or 20 minutes pass. */
|
|
170
|
+
export function setWorking(dir, on) { return updatePiece(dir, (p) => { delete p.ask; p.working = { on: String(on ?? '').trim() || null, at: now() }; }); }
|
|
171
|
+
/** A short reply from the agent to the user, shown in the studio's thread. */
|
|
172
|
+
export function addNote(dir, text) { return updatePiece(dir, (p) => { (p.notes ??= []).push({ at: now(), text: String(text).trim() }); }); }
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* What the studio may say about the agent, only from facts: `ask` (a question is open), `listen` (a command of the
|
|
176
|
+
* agent is waiting on this piece right now), `work` (it said what it is doing, less than 20 minutes ago) or `off`.
|
|
177
|
+
*/
|
|
178
|
+
export function agentState(dir, p = readPiece(dir)) {
|
|
179
|
+
const l = listening(dir), w = p?.working && Date.now() - Date.parse(p.working.at) < WORKING_FOR ? p.working : null;
|
|
180
|
+
return { state: p?.ask ? 'ask' : l ? 'listen' : w ? 'work' : 'off', listening: !!l, working: w };
|
|
181
|
+
}
|
|
182
|
+
|
|
123
183
|
// an event that ends the agent's wait: a decision on the stage under review, or a change asked anywhere
|
|
124
184
|
const decisive = (e, stage) => (e.kind === 'approve' && e.stage === stage && !e.part) || e.kind === 'changes' || e.kind === 'reopen' || e.rejoin || e.refinal;
|
|
125
185
|
|
|
@@ -152,7 +212,15 @@ const beatFile = (dir) => path.join(dir, '.agent.json');
|
|
|
152
212
|
export function beat(dir, info = {}) { try { fs.writeFileSync(beatFile(dir), JSON.stringify({ at: Date.now(), pid: process.pid, ...info })); } catch { /* a read-only project: the studio just won't know */ } }
|
|
153
213
|
export function unbeat(dir) { try { if (JSON.parse(fs.readFileSync(beatFile(dir), 'utf8')).pid === process.pid) fs.rmSync(beatFile(dir)); } catch { /* already gone */ } }
|
|
154
214
|
/** The command waiting on this piece right now, if any. */
|
|
155
|
-
export function listening(dir, within = 9000) {
|
|
215
|
+
export function listening(dir, within = 9000) {
|
|
216
|
+
try {
|
|
217
|
+
const a = JSON.parse(fs.readFileSync(beatFile(dir), 'utf8'));
|
|
218
|
+
if (Date.now() - a.at >= within) return null;
|
|
219
|
+
// a command that was killed leaves its last beat behind: it only counts while its process is alive
|
|
220
|
+
if (a.pid) { try { process.kill(a.pid, 0); } catch (e) { if (e.code === 'ESRCH') return null; } }
|
|
221
|
+
return a;
|
|
222
|
+
} catch { return null; }
|
|
223
|
+
}
|
|
156
224
|
|
|
157
225
|
/** The agent asked the user something in the chat: the studio says so until the agent moves on. */
|
|
158
226
|
export function setAsk(dir, text) { return updatePiece(dir, (p) => { p.ask = { text: String(text ?? '').trim(), at: now() }; }); }
|
|
@@ -196,6 +264,49 @@ export function setSceneText(html, key, value) {
|
|
|
196
264
|
return head + next + tail;
|
|
197
265
|
}
|
|
198
266
|
|
|
267
|
+
// ── choices: a board shown inside the studio instead of a link (ADR 0005) ─────────────────────────────────────
|
|
268
|
+
/** Puts a board into the piece, on the step and section it decides. Returns its id (`c1`, `c2`…). */
|
|
269
|
+
export function addChoice(dir, { board, where }) {
|
|
270
|
+
let id;
|
|
271
|
+
updatePiece(dir, (p) => {
|
|
272
|
+
p.choices ??= [];
|
|
273
|
+
id = `c${p.choices.length + 1}`;
|
|
274
|
+
p.choices.push({ id, board, where, status: 'waiting', at: now() });
|
|
275
|
+
});
|
|
276
|
+
return id;
|
|
277
|
+
}
|
|
278
|
+
/** The user's pick on a choice: records the reply line and an event the agent reads. */
|
|
279
|
+
export function settleChoice(p, id, line) {
|
|
280
|
+
const c = (p.choices ?? []).find((x) => x.id === id);
|
|
281
|
+
if (!c) throw new Error('no such choice');
|
|
282
|
+
if (c.status !== 'waiting') throw new Error('already chosen');
|
|
283
|
+
c.status = 'chosen'; c.line = line; c.chosenAt = now();
|
|
284
|
+
addEvent(p, { kind: 'choice', stage: c.where?.step ?? p.stage, text: line, title: c.board.title, choice: id });
|
|
285
|
+
return c;
|
|
286
|
+
}
|
|
287
|
+
/** Waits for the user's pick on a studio choice, beating as listening; returns the reply line, or null on timeout. */
|
|
288
|
+
export async function awaitChoiceIn(dir, id, { timeout = 10 * 60 * 1000, every = 1000 } = {}) {
|
|
289
|
+
const t0 = Date.now(); let last = 0;
|
|
290
|
+
const stop = () => { unbeat(dir); process.exit(130); };
|
|
291
|
+
process.once('SIGINT', stop); process.once('SIGTERM', stop);
|
|
292
|
+
try {
|
|
293
|
+
for (;;) {
|
|
294
|
+
if (Date.now() - last > 3000) { beat(dir, { waiting: 'board' }); last = Date.now(); }
|
|
295
|
+
const c = (readPiece(dir)?.choices ?? []).find((x) => x.id === id);
|
|
296
|
+
if (!c) return null;
|
|
297
|
+
if (c.status === 'chosen') {
|
|
298
|
+
// the agent has the pick from here: the event that carries it counts as read
|
|
299
|
+
updatePiece(dir, (p) => { const at = now(); for (const e of p.events) if (e.kind === 'choice' && e.choice === id && !e.readAt) { e.readAt = at; if (e.n === (p.seen ?? 0) + 1) p.seen = e.n; } });
|
|
300
|
+
return c.line;
|
|
301
|
+
}
|
|
302
|
+
if (Date.now() - t0 > timeout) return null;
|
|
303
|
+
await new Promise((res) => setTimeout(res, every));
|
|
304
|
+
}
|
|
305
|
+
} finally { unbeat(dir); process.off('SIGINT', stop); process.off('SIGTERM', stop); }
|
|
306
|
+
}
|
|
307
|
+
/** Takes a choice back out of the studio. */
|
|
308
|
+
export function forgetChoice(dir, id) { return updatePiece(dir, (p) => { p.choices = (p.choices ?? []).filter((x) => x.id !== id); }); }
|
|
309
|
+
|
|
199
310
|
// ── boards: the decisions taken on paper-motion.app, listed in the studio so the user finds them there too ──────
|
|
200
311
|
const BOARD_STAGE = () => 'video';
|
|
201
312
|
|
|
@@ -204,7 +315,7 @@ export function activePiece(cwd, name) {
|
|
|
204
315
|
if (name) { const dir = pieceDir(name, cwd); return readPiece(dir) ? dir : null; }
|
|
205
316
|
const root = path.join(cwd, 'pieces');
|
|
206
317
|
if (!fs.existsSync(root)) return null;
|
|
207
|
-
const live = fs.readdirSync(root).map((d) => path.join(root, d)).filter((d) => { const p = readPiece(d); return p && ['brief', 'inventory', 'video'].includes(p.stage); });
|
|
318
|
+
const live = fs.readdirSync(root).map((d) => path.join(root, d)).filter((d) => { const p = readPiece(d); return p && ['brief', 'inventory', 'look', 'video'].includes(p.stage); });
|
|
208
319
|
return live.length === 1 ? live[0] : null;
|
|
209
320
|
}
|
|
210
321
|
|
package/lib/registry.mjs
CHANGED
|
@@ -122,7 +122,11 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
122
122
|
(an approval, a change to make, or an edit they already made by hand: the brief, words on screen, lengths, joins,
|
|
123
123
|
order; build on those, never undo them). Quick renders for review: \`chain <piece> --drafts\` (each part on its
|
|
124
124
|
own) and \`chain <piece> --draft\` (joined). Only when the user can't open a browser, run the same stages in the
|
|
125
|
-
chat.
|
|
125
|
+
chat. Between your turns keep \`npx paper-motion piece listen <piece>\` running in the background: it returns as
|
|
126
|
+
soon as the user needs you (a message, an answer, an approval), with lines like review's, so they never have to
|
|
127
|
+
come back to the chat (start it again after; exit 3 means nothing in 9 minutes). Say what you are doing with
|
|
128
|
+
\`piece working <piece> <stage> --on "…"\`, ask with \`piece ask <piece> "…"\` (they answer in the studio), and
|
|
129
|
+
reply there with \`piece say <piece> "…"\`. The studio only shows what it can check about you.
|
|
126
130
|
3. **Interview.** Follow the \`make-video\` skill. Without the skills, ask at least: what the brand is in one plain
|
|
127
131
|
line; what this video is for and where it plays (format: 16:9, 9:16, 1:1); its ground, text and accent colors or
|
|
128
132
|
"no brand yet"; three words for the feel and one thing it must never look like; the one action at the end.
|
|
@@ -164,9 +168,11 @@ export function llmsTxt(items, site, { full = false } = {}) {
|
|
|
164
168
|
builds the video itself from what they kept, and then they edit it there, live (words, how each part arrives,
|
|
165
169
|
backgrounds, settings, lengths, transitions, order, adding and swapping parts, colours, type, pace); every edit
|
|
166
170
|
reaches you as a line. Review \`video\` to wait for their approval; they can render the final from the studio. \`chain --check\` warns when the total drifts from the brief's
|
|
167
|
-
length.
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
length. While a piece is open and its studio runs, \`board --url\` puts the options into the studio, on the step
|
|
172
|
+
they decide (How it looks for palettes, type, backgrounds and moves; Your video for transitions and structures),
|
|
173
|
+
instead of making a link; it waits there and prints the same reply line, and a palette, font pair or background
|
|
174
|
+
the user picks is applied already. \`--link\` makes a link anyway. If your shell caps how long a command may run,
|
|
175
|
+
use \`--no-wait\` and then \`npx paper-motion board --await <c1…> --wait 2\` (exit code 3 = not yet).
|
|
170
176
|
|
|
171
177
|
## Choosing well: the metadata
|
|
172
178
|
|