dsh-claude-motion 2.11.0 → 2.12.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.
Files changed (26) hide show
  1. package/lib/index.js +1 -1
  2. package/package.json +14 -14
  3. package/skills/claude-motion/SKILL.md +138 -108
  4. package/skills/claude-motion/artifact-type/reference/by-hand.md +23 -9
  5. package/skills/claude-motion/artifact-type/reference/design-systems.md +50 -0
  6. package/skills/claude-motion/artifact-type/reference/film-api.md +129 -252
  7. package/skills/claude-motion/artifact-type/reference/film-folder.md +107 -0
  8. package/skills/claude-motion/artifact-type/reference/finalize.md +118 -203
  9. package/skills/claude-motion/artifact-type/reference/kinds.md +43 -0
  10. package/skills/claude-motion/artifact-type/reference/later.md +130 -0
  11. package/skills/claude-motion/artifact-type/reference/motion.md +43 -239
  12. package/skills/claude-motion/artifact-type/reference/plan.md +4 -4
  13. package/skills/claude-motion/artifact-type/reference/questions.md +91 -152
  14. package/skills/claude-motion/artifact-type/reference/recording.md +186 -0
  15. package/skills/claude-motion/artifact-type/reference/recreate-ui.md +128 -3
  16. package/skills/claude-motion/artifact-type/reference/render-offline.md +3 -1
  17. package/skills/claude-motion/artifact-type/reference/rough-cut.md +109 -213
  18. package/skills/claude-motion/artifact-type/reference/sound.md +49 -100
  19. package/skills/claude-motion/artifact-type/reference/voice.md +63 -0
  20. package/skills/claude-motion/artifact-type/reference/walkthrough.md +4 -4
  21. package/skills/claude-motion/scripts/artifact.mjs +96 -9
  22. package/skills/claude-motion/scripts/editor/artifact-type/app.css +1 -1
  23. package/skills/claude-motion/scripts/editor/artifact-type/app.js +1177 -823
  24. package/skills/claude-motion/scripts/editor/index.html +1 -1
  25. package/skills/claude-motion/scripts/editor/local-host.js +6 -6
  26. package/skills/claude-motion/scripts/editor/share-ui.js +10 -10
@@ -1,106 +1,55 @@
1
- # Sound — a score generated from the cut
2
-
3
- The score is part of the film: `window.CUT.score(S)` declares notes and
4
- foley against the SAME timeline constants the picture uses, the player
5
- synthesizes it (a small built-in WebAudio synth — no samples, no
6
- soundfont) in sync with playback, and Export renders the
7
- identical events into the MP4's audio track. `S` only records events;
8
- it is deterministic data, so write it like the picture: from `TT`.
9
-
10
- ## The method (what worked in review)
11
-
12
- - **Pick the tempo from the cut, not the other way round.** One beat =
13
- the hard-cut cadence of your showcase run (0.63 s → 95 BPM in the
14
- reference); phase the grid (`t0`) so bar lines land on the big
15
- moments — the first close-up, the editor landing, the end page. If a
16
- visual beat misses a downbeat by a few frames, retime the BEAT in
17
- `TT`: nobody sees three frames, everybody hears an off-beat hit.
18
- - **Groove on the grid, foley on exact frames.** Music events take
19
- beats; UI foley takes the precise t of the event it belongs to, off
20
- grid: a wood-block tick per visible keystroke (your typist's
21
- `times`), side-stick clicks on clicks, a double tick for a
22
- double-click, pops as list items or capsules appear, a rising roll
23
- while something is dragged or scrubbed with a ding when the value
24
- lands, a two-note chime on Save, a whoosh under a whip-pan, a boing
25
- when something drops in and squashes.
26
- - **Arc** (keep the dynamics; don't flatten them): near-silent title
27
- (a pad, maybe one mallet note) → a light pulse under the typing →
28
- the groove enters when the action starts → stabs on the hard-cut
29
- section → drop to a pad for the transform → groove B under the
30
- person's edit → a short build (an accelerating run) INTO the hit as
31
- the end page lands → resolve and let the last chord ring; fade by the
32
- last second. Stop-time before the biggest hit is free drama.
33
- - **Instrumentation that suits a product film**: marimba or kalimba ostinato,
34
- electric piano chords, a round fingered bass, a light kit (kick, side
35
- stick, closed hat, a shaker at most), a soft pad. No risers, no
36
- cinematic booms, no vocal chops. Major or modal, mid-tempo, warm.
37
- - **Your own tune.** Write no existing song's, theme's or jingle's melody, note by note or near
38
- enough to be recognised: write an original one in that mood. That you did goes in the close's one
39
- sentence more (what you could not do as asked): it names the song they asked for, not your music.
40
- The user's own, or in the public domain: fine. A genre, a mood, a tempo, an instrument: fine.
41
- - **They asked for other music: name it in a plain line**: what you wrote (a mood you aimed
42
- for, two instruments in everyday words: a marimba, plucked notes, an electric piano, a
43
- shaker), never how it sounds. No key, BPM or apology. Mellower, no typing ticks, other
44
- instruments: a one-line change. Music off by request is `score` returning nothing; mute and
45
- volume are the viewer's own.
46
-
47
- ## `score(S, kit)`
48
-
49
- Runs once at load, after `init` (so typists made in `init` have
50
- filled `kit.typing.times`). `S` only records events — pure data.
1
+ # Sound: a score generated from the cut
2
+
3
+ The score is data. `S` only records notes and effects against the same timeline the picture uses, the player synthesizes it
4
+ in sync, and Export renders the same events into the file.
5
+
6
+ A voiceover is not part of the score. If they asked for one, or to use an audio recording or music file of theirs, the close
7
+ says there is none, in the words of `artifact-type/reference/voice.md` (read it if it isn't in this conversation; the installed skill:
8
+ `voice.md`, beside this page). Where the film speaks, recording.md's close stands instead of the voiceover's sentence; one
9
+ about their recording or music file stays.
10
+
11
+ ## The method
12
+
13
+ - **Pick the tempo from the cut.** One beat is the film's cut rhythm. Land bar lines on the big moments, and if a visual beat
14
+ misses a downbeat by a few frames, retime the beat: nobody sees three frames, and everybody hears an off-beat hit.
15
+ - **Groove on the grid, effects on exact frames.** Music takes beats. An effect takes the exact `t` of the event it belongs to.
16
+ - **Sound the world.** What the viewer sees should be heard: a `whoosh` as wind crosses, `thud` on a landing, `pop` as things
17
+ appear, ticks for a clock.
18
+ - **Keep an arc.** Start near silent, bring the groove in with the action, drop back before the end, build into the hit as the
19
+ end card lands, then let the last chord ring. Make the hit clearly louder than the opening.
20
+ - **Let the world pick the instruments.** A lullaby wants glockenspiel and pad, a foundry low pads and struck metal, a product
21
+ film marimba, electric piano, round bass and a light kit. No risers, cinematic booms or vocal chops.
22
+ - **Your own tune.** Write no existing song's, theme's or jingle's melody, note by note or near enough to be recognised: write
23
+ an original one in that mood, and say so in the close's one sentence (it names the song they asked for, not your music). The
24
+ user's own, or the public domain, is fine. A genre, mood, tempo or instrument is fine.
25
+ - **On a later turn, if they asked for other music, name it in a plain line:** a mood and two instruments in everyday words, with no key, BPM or
26
+ apology. Music off by request is a film with no score; mute and volume are the viewer's own.
27
+
28
+ ## `S`
51
29
 
52
30
  ```js
53
- score(S, kit) {
54
- const BPM = 95.24, B = 60 / BPM;
55
- S.tempo(BPM, { t0: TT.cut1 - 16 * B }); // grid phased so beat 16 IS the first close-up
56
- S.program('keys', 'ep'); S.program('bass', 'bass'); S.program('mallet', 'marimba'); S.program('pad', 'pad', { rev: 0.6 });
57
- S.note('pad', 'D3', { beat: 0, beats: 8, vel: 30 }); // {beat, beats} on the grid, or {t, dur} in seconds
58
- S.chord('keys', ['D3', 'A3', 'D4', 'F#4'], { beat: 16, beats: 4, vel: 52 });
59
- S.note('bass', 'D2', { beat: 16, beats: 1.5, vel: 78 });
60
- S.pattern('mallet', 'x.x.X.xx', { from: 16, repeat: 4, pitch: ['D5', 'A4'], vel: 64 }); // step 0.5 beat; X accents
61
- S.drums({ kick: 'x...x...', sidestick: '..x...x.', hat: 'oxoxoxox' }, { from: 16, repeat: 8 }); // o = ghost note
62
- S.ticks(kit.typing.times, { vel: 30 }); // one wood tick per keystroke, timbre-jittered
63
- S.fx('click', TT.send); S.fx('pop', TT.chip); S.fx('roll', TT.dragA, { dur: TT.dragB - TT.dragA }); // roll lands on a ding
64
- S.fx('chime', TT.saved); S.fx('whoosh', TT.whip, { dur: 0.5 }); S.fx('revswell', TT.end, { dur: 1.2 }); // swells INTO TT.end
65
- S.run('mallet', ['D5', 'E5', 'F#5', 'A5', 'B5', 'D6'], { t: TT.end - 0.62, step: 0.1, vel: 50, cresc: 8 });
31
+ kit.score(function (S, T) {
32
+ const B = 60 / 96;
33
+ S.tempo(96, { t0: T.pour - 8 * B }); // bar lines land on the pour
34
+ S.program('pad', 'pad', { rev: 0.6 }); S.program('bass', 'bass'); S.program('mallet', 'marimba');
35
+ S.note('pad', 'D3', { t: 0, dur: 3, vel: 30 }); // {t, dur} in seconds, or {beat, beats} on the grid
36
+ S.chord('pad', ['D3', 'A3', 'F#4'], { beat: 8, beats: 4, vel: 52 });
37
+ S.pattern('mallet', 'x.x.X.xx', { from: 8, repeat: 4, pitch: ['D5', 'A4'], vel: 64 }); // step 0.5 beat; X accents
38
+ S.drums({ kick: 'x...x...', hat: 'oxoxoxox' }, { from: 8, repeat: 4 }); // o is a ghost note
39
+ S.fx('whoosh', T.ring, { dur: 0.5 }); S.fx('chime', T.come + 0.4);
66
40
  S.master({ gain: -6, fadeOut: 1.2 });
67
- }
41
+ });
68
42
  ```
69
43
 
70
- - `S.tempo(bpm, {t0})` sets the grid; `S.beat(n)` → seconds,
71
- `S.toBeat(t)` → beats. Everything takes `{beat, beats}` (grid) or
72
- `{t, dur}` (seconds).
73
- - `S.program(track, patch, {pan −1..1, gain dB, rev 0..1, bright
74
- 0..2}?)` names a track. Pitched patches: `marimba`, `vibes`,
75
- `glock`, `ep` (electric piano), `keys` (soft synth), `pluck`
76
- (kalimba-ish), `bass`, `pad`. Drums: `kick`, `snare`, `clap`,
77
- `sidestick`, `hat`, `openhat`, `shaker`, `tamb`, `woodblock`,
78
- `cowbell`, `tom`, `crash`, `ride`.
79
- - `S.note(track, 'F#4' | midi, {…, vel 0–127})`, `S.chord(track,
80
- [pitches], {…, spread})`, `S.pattern(track, 'x.X.', {from, step =
81
- 0.5, repeat, pitch: [cycle], vel, dur})`, `S.run(track, [pitches],
82
- {t, step, vel, cresc})`, `S.drum(name, {beat | t, vel})`,
83
- `S.drums({name: 'x..o'}, {from, step, repeat, vel})` (`X` accent,
84
- `o` ghost). A little humanizing is built in (`S.seed(n)` varies it).
85
- - Foley at exact seconds, `S.fx(name, t, {dur, vel, pitch, pan}?)`:
86
- `tick` (keystroke), `click`, `doubleclick`, `pop`, `ding` (two
87
- notes a fifth apart), `chime` (a small arpeggio — Save), `sparkle`,
88
- `whoosh` (`{into: 1}` reverses it), `revswell` (swells INTO its
89
- `t` over `dur`), `roll` (accelerating ticks over `dur`, a ding on
90
- landing — drags and scrubs), `boing` (a bouncy landing), `thud`.
91
- `S.ticks(times, {vel, jitter})` = one tick per time.
92
- - `S.master({gain dB = −6, fadeOut s = 1})`. Keep the arc: the hit
93
- should be clearly louder than the intro pad.
44
+ A film with no list of scenes has no `kit.score`: the same body goes in `window.CUT`'s `score`.
45
+
46
+ `note`, `chord` and `drum` take `{beat, beats}` or `{t, dur}`. `S.beat(n)` gives seconds. Also `S.drum(name, {beat|t, vel})`,
47
+ `S.run(track, [pitches], {t, step, vel, cresc})` and `S.ticks(times, {vel})`, one tick per time.
48
+
49
+ - Pitched patches: `marimba`, `vibes`, `glock`, `ep`, `keys`, `pluck`, `bass`, `pad`. `S.program(track, patch, {pan, gain, rev, bright})`.
50
+ - Drums: `kick`, `snare`, `clap`, `sidestick`, `hat`, `openhat`, `shaker`, `tamb`, `woodblock`, `cowbell`, `tom`, `crash`, `ride`.
51
+ - Effects, `S.fx(name, t, {dur, vel, pitch, pan})`: `tick`, `click`, `doubleclick`, `pop`, `ding`, `chime`, `sparkle`, `whoosh`
52
+ (`{into: 1}` reverses it), `revswell` (swells into its `t` over `dur`), `roll`, `boing`, `thud`.
94
53
 
95
- `pattern`/`drums` with `repeat` happily overrun the beat you meant to
96
- stop at — compute the repeat count (or the step string) to fit
97
- [t0, t1). Unknown drum or foley names are SILENT (an unknown pitched
98
- patch falls back to marimba); `check.ts` lists them with the event
99
- count, tempo and sounding span. An event timed 0.1 s or more before 0
100
- is dropped without a word (one closer is moved to 0), so the sound for
101
- an entrance that starts early goes at 0. Listen by numbers: `export.ts --no-video --levels`
102
- prints loudness per second (RMS dBFS: −60 silent, −30 quiet bed, −20
103
- groove, −12 hit) — no second between the title and the sign-off should
104
- sit below about −30; `--mid score.mid` added to them writes a Standard
105
- MIDI File on your grid for a composer.
106
- Keep it small — a 35 s score is a few hundred events.
54
+ An unknown drum or effect is silent. `pattern` and `drums` with `repeat` overrun happily, so compute the count to fit. An event
55
+ timed 0.1 s or more before 0 is dropped, so the sound for an early entrance goes at 0. A 30 s score is a few hundred events.
@@ -0,0 +1,63 @@
1
+ # Asked for a voiceover
2
+
3
+ This page says what to tell someone who asks for a voiceover or a narration, or to use an audio recording or music file
4
+ of theirs, and where in the chat to say it. Also read all of `artifact-type/reference/recording.md` (the installed skill:
5
+ `recording.md`, beside this page) if your tools can record a voice (your `Artifact` tool lists the action `speak`, or
6
+ your tools list `mcp__claude_ai__artifact_speak`; go by the names you see, and don't search for them), or if the film
7
+ speaks already (it holds `modules/narration.js`, or its `film/script` sets `narration`: look in its data, with a `list`
8
+ of `modules` and a `get` of `film/script`). Where that page differs from this one or from SKILL.md, it goes first.
9
+ Without either, Motion adds no voice and never offers one; either way, it never uses an audio recording or music file
10
+ of theirs. **A new film is made all the same,** with the words on screen in its scenes.
11
+
12
+ ## What they are told
13
+
14
+ | They want | Ahead of the form | In the form | In the close |
15
+ | --- | --- | --- | --- |
16
+ | a voiceover, a narration, anyone's voice (you can't record) | I can't add a voiceover. | There will be no voiceover: the words will be on screen. | It has no voiceover, since I can't add one: the words are on screen. |
17
+ | their own audio recording in it, even as the voiceover (use this row, not the first) | I can't use your recording. | Your recording can't be used: the animation will have text on screen. Once it is finished, Export saves it as a video, and any video editor can add your recording to that. | I couldn't use your recording, but any video editor can add it to the video that Export saves. |
18
+ | a music file of theirs in it | I can't use your music file. | Your music file can't be used: I'll compose a score. | I couldn't use your music file, but any video editor can add it to the video that Export saves. |
19
+
20
+ Say it in their language, and as short. It is a plain fact about Motion, not a refusal: give no reason and no word of
21
+ time or place (not even "here"), and never let it hold up the form or the film. A tip or an offer may go anywhere but
22
+ in the line ahead of the form.
23
+
24
+ ## Where
25
+
26
+ Chat can hide what you write ahead of a tool call. So they are told in **the last text of the turn in which they ask**:
27
+
28
+ - **A mere question** ("can it do voiceover?"): "No, I can't add a voiceover to an animation." Start nothing.
29
+ - **A film they have already seen:** do the rest of the ask as usual, and say "I can't add a voiceover." last (in a
30
+ close: these same words, where the close bullet puts them). You may offer to put the words on screen, if they aren't there.
31
+ - **A question or a refusal of your own that ends the turn:** "I can't add a voiceover, so I'll put the words on
32
+ screen." goes with it. When kinds.md turns the whole film down, leave the voice out of that no (it is two sentences,
33
+ with no "can't"). If they take the film it offers, tell them as for any new film.
34
+ - **A close** (of a film, a plan or a rough cut): a sentence of its own, after its first sentence ("Here's your animation.")
35
+ and the link, if there is one. It isn't the close's one short sentence more.
36
+
37
+ **A new film, with the form.** That turn ends on the form, with nothing after it. So they are told in three places:
38
+ ahead of the form, in it, and again in the next close. Each is needed; none stands in for another.
39
+
40
+ - **Ahead of it:** the sentence goes into SKILL.md's one line before the form, and nothing else of yours goes there.
41
+
42
+ > I can't add a voiceover. A few quick picks, then I'll start.
43
+
44
+ Where that line opens with "Your animation is set up.", the sentence comes next:
45
+
46
+ > Your animation is set up. I can't add a voiceover. A few quick picks, then I'll start.
47
+
48
+ In any language but English the sentence is the whole line, without even "Your animation is set up.": the line must
49
+ stay far under 200 bytes. Write the line last, with no call or thought between it and `show_widget`, or
50
+ `ask_user_input_v0` (which has no such line: the sentence alone).
51
+ - **In it:** put the form's words into questions.md's form as a `<p>`, first inside its `elicit-body`. Anything else
52
+ that can't wait for the close (the answer to a question of theirs, the note questions.md puts in the line before the
53
+ form) follows them there, never in the line.
54
+ With `ask_user_input_v0`, the first sentence of the form's words opens the first question's text, whichever it is:
55
+ "There will be no voiceover: the words will be on screen. Which style should it have?" Keep that text under about 120
56
+ characters. What doesn't fit, an answer to a question of theirs too, waits for the close.
57
+
58
+ ## Never
59
+
60
+ - No speech made by code (`claude.use("tts")` is silent), no audio file as an asset, and don't ask for a recording or wait
61
+ for one.
62
+ - No promise of a voice at any other time or place.
63
+ - A push changes nothing: give the same answer.
@@ -47,15 +47,15 @@ doesn't move the story goes. Chapters become `window.CUT.chapters`
47
47
  stagger, 34 px drift), a 40 px subhead; 2.4–3.2 s. Then the quad **morphs into
48
48
  the UI window** (1.1–1.2 s) while the screen fades up inside it — no
49
49
  hard cuts from slides into UI.
50
- - **Personas** (multi-flow only): a 220 px flat smiley in ONE secondary
51
- color (never touching the accent) whose eyes and smile are ink marks
52
- that draw on and boil; name 40 px semibold, role 30 px, their need as a
50
+ - **Personas** (multi-flow only): a 220 px flat profile, head and shoulders, in
51
+ ONE secondary color (never touching the accent), whose outline is an
52
+ ink mark that draws on and boils; name 40 px semibold, role 30 px, their need as a
53
53
  44 px display line from the right; ~3.5 s;
54
54
  then it flies down to a 72 px badge bottom-left and rides there
55
55
  through that flow's beats.
56
56
  - **No speech bubbles over UI.** When the person has something to say
57
57
  mid-flow, use an interstitial text card: the UI eases to scale 0.96 /
58
- opacity 0.25, one 72 px display line (and the small smiley) scales
58
+ opacity 0.25, one 72 px display line (and the small profile) scales
59
59
  0.92 → 1 over 1.4 s, holds 1.6 s, the UI eases back.
60
60
  - **Closing card**: a 72 px display line from the right, one accent
61
61
  recap chip per flow tumbling in (2.5D is allowed for shapes), a tiny grey
@@ -1415,8 +1415,11 @@ var ShareHost = class {
1415
1415
  * their own artifact; a guest comes over a LAN address (this computer's own included: that is how the owner sees
1416
1416
  * what a role sees).
1417
1417
  */
1418
+ isLocal(request) {
1419
+ return this.options.local ? this.options.local(request) : isLoopback(request.socket.remoteAddress);
1420
+ }
1418
1421
  viewerOf(request) {
1419
- if (isLoopback(request.socket.remoteAddress)) return { role: "owner", user: this.options.owner };
1422
+ if (this.isLocal(request)) return { role: "owner", user: this.options.owner };
1420
1423
  const guest = this.on ? this.sharing.guest(this.cookie(request)) : null;
1421
1424
  if (!guest) return null;
1422
1425
  this.sharing.seen(guest);
@@ -1510,6 +1513,7 @@ var ShareHost = class {
1510
1513
  const share2 = { ...this.me(viewer), noun: this.options.noun, title: this.options.title(), ...this.options.reactions ? { reactions: true } : {} };
1511
1514
  let js = `(function(BASE){${ROOT_SHIM}})(${script(this.page)});${SECURE_CONTEXT_SHIM}window.__baoyuShare=${script(share2)};`;
1512
1515
  for (const [name, value] of Object.entries(this.options.globals?.(viewer, query) ?? {})) js += `window[${script(name)}]=${script(value)};`;
1516
+ if (this.options.agentName) js += `window.__baoyuAgentName=${script(this.options.agentName)};`;
1513
1517
  if (viewer.guest) js += GUEST_WATCH;
1514
1518
  return js;
1515
1519
  }
@@ -1548,7 +1552,7 @@ var ShareHost = class {
1548
1552
  async join(request, response, method, token2) {
1549
1553
  const t = this.words(request);
1550
1554
  const role = this.sharing.linkRole(token2);
1551
- const local = isLoopback(request.socket.remoteAddress);
1555
+ const local = this.isLocal(request);
1552
1556
  const form = method === "POST" ? await readBody(request, 8 << 10).then((body) => body.toString("utf8"), () => "") : "";
1553
1557
  const title = this.options.title();
1554
1558
  if (!role || !local && !this.on) return this.sendPage(response, 404, t.linkGoneTitle, `<h1>${esc(t.linkGoneTitle)}</h1><p>${esc(t.linkGone)}</p>`);
@@ -3412,6 +3416,7 @@ data: ${JSON.stringify(snap)}
3412
3416
  if (agentHook) await runHook(agentHook);
3413
3417
  }
3414
3418
  async function runHook(command2) {
3419
+ if (typeof command2 !== "string") return runHandler(command2);
3415
3420
  const doc = await readComments(root);
3416
3421
  for (const thread of pendingForAgent(doc)) {
3417
3422
  if (hookRunning.has(thread.id)) continue;
@@ -3436,15 +3441,15 @@ data: ${JSON.stringify(snap)}
3436
3441
  log3(`comments: the agent hook failed to start: ${error.message}`);
3437
3442
  });
3438
3443
  child.on("exit", (code) => {
3439
- hookRunning.delete(thread.id);
3440
3444
  if (code !== 0) {
3445
+ hookRunning.delete(thread.id);
3441
3446
  log3(`comments: the agent hook exited with ${code} for ${thread.id}; the comment stays waiting${stderr ? `: ${stderr.trim()}` : ""}`);
3442
3447
  return;
3443
3448
  }
3444
3449
  void updateComments(root, (d) => {
3445
3450
  const t = d.threads.find((x) => x.id === thread.id);
3446
3451
  if (t) markDelivered(t, now(), "hook");
3447
- }).then(() => {
3452
+ }).finally(() => hookRunning.delete(thread.id)).then(() => {
3448
3453
  log3(`comments: sent to the agent (hook): ${thread.id}`);
3449
3454
  return broadcast();
3450
3455
  }).catch((error) => log3(`comments: ${error.message}`));
@@ -3485,6 +3490,44 @@ data: ${JSON.stringify(snap)}
3485
3490
  });
3486
3491
  }
3487
3492
  }
3493
+ async function runHandler(handler) {
3494
+ for (const { id } of pendingForAgent(await readComments(root))) {
3495
+ if (hookRunning.has(id)) continue;
3496
+ hookRunning.add(id);
3497
+ try {
3498
+ const doc = await readComments(root);
3499
+ const thread = pendingForAgent(doc).find((t) => t.id === id);
3500
+ if (!thread) continue;
3501
+ try {
3502
+ await handler.thread(agentViews(doc, [thread])[0]);
3503
+ } catch (error) {
3504
+ log3(`comments: the agent handler failed for ${id}; the comment stays waiting: ${error.message}`);
3505
+ continue;
3506
+ }
3507
+ await updateComments(root, (d) => {
3508
+ const t = d.threads.find((x) => x.id === id);
3509
+ if (t) markDelivered(t, now(), "hook");
3510
+ });
3511
+ log3(`comments: sent to the agent (handler): ${id}`);
3512
+ await broadcast();
3513
+ } finally {
3514
+ hookRunning.delete(id);
3515
+ }
3516
+ }
3517
+ const failed = [];
3518
+ for (const request of requests.splice(0)) {
3519
+ setRequestState(request.id, "working");
3520
+ try {
3521
+ await handler.request(request);
3522
+ log3(`comments: sent to the agent (handler): ${request.id}`);
3523
+ } catch (error) {
3524
+ failed.push(request);
3525
+ setRequestState(request.id, "queued");
3526
+ log3(`comments: the agent handler failed for ${request.id}; the request stays waiting: ${error.message}`);
3527
+ }
3528
+ }
3529
+ requests.unshift(...failed);
3530
+ }
3488
3531
  let debounce;
3489
3532
  const changed = () => {
3490
3533
  clearTimeout(debounce);
@@ -3623,6 +3666,21 @@ data: ${JSON.stringify(snap)}
3623
3666
  } else if (parts.length === 2 && parts[0] === "agent" && parts[1] === "wait") {
3624
3667
  need(viewer.canSend, "Only the owner's agent waits for comments.");
3625
3668
  if (reading) return await wait(request, response, url);
3669
+ } else if (parts.length === 2 && parts[0] === "agent" && parts[1] === "send-all") {
3670
+ if (method === "POST") {
3671
+ request.resume();
3672
+ need(viewer.canSend, ONLY_OWNER_SENDS);
3673
+ const result = await updateComments(root, (doc) => {
3674
+ const open2 = doc.threads.filter((t) => t.status === "open" && t.messages.some((m) => m.author.kind === "user"));
3675
+ const sent = open2.filter(canSendToAgent);
3676
+ const at = now();
3677
+ for (const t of sent) sendToAgent(t, at);
3678
+ return { threads: sent, sent: { count: sent.length, open: open2.length } };
3679
+ });
3680
+ log3(`comments: ${result.sent.count} of ${result.sent.open} open thread(s) sent to the agent`);
3681
+ sendJson2(response, 200, result);
3682
+ return await afterChange(result.sent.count > 0);
3683
+ }
3626
3684
  } else if (parts.length === 2 && parts[0] === "agent" && parts[1] === "requests") {
3627
3685
  need(viewer.canSend, ONLY_OWNER_SENDS);
3628
3686
  if (method === "POST") return await ask(request, response);
@@ -6602,6 +6660,7 @@ var BLOB_ID = /^[0-9a-f]{32}$/;
6602
6660
  var DATA_URI = /^data:(image\/(png|jpeg|gif|webp|svg\+xml)|font\/(woff2|woff|ttf|otf)|video\/(mp4|webm)|audio\/(mpeg|wav));base64,[A-Za-z0-9+/=]+$/;
6603
6661
  var MODULE_KINDS = ["js", "css", "html", "json", "text"];
6604
6662
  var ASSET_MIMES = ["image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml", "font/woff2", "font/woff", "font/ttf", "font/otf", "video/mp4", "video/webm", "application/pdf", "audio/mpeg", "audio/wav"];
6663
+ var REPLACE_ONLY_DOCS = /* @__PURE__ */ new Set(["modules/fetched-fonts.json", "modules/recolored.json", "modules/retouched.json", "modules/retyped.json"]);
6605
6664
  var UNSPLIT_MODULES = ["edits.json", "fetched-fonts.json", "recolored.json", "recut.json", "refaced.json", "retouched.json", "retyped.json", "tweaks.json"];
6606
6665
  var CONTENT_TYPES = {
6607
6666
  html: "text/html; charset=utf-8",
@@ -6790,6 +6849,7 @@ function documentProblem(root, segs, data) {
6790
6849
  if (mime === "image/svg+xml") return `assets/${id}: an SVG is never uploaded; put its <svg> markup in an html module, or a data:image/svg+xml;base64,\u2026 ref.`;
6791
6850
  if (!fs19.existsSync(blobFile(root, blob[1]))) return `assets/${id}: ${ref} is not in this film's _blobs/. Upload the file first (${cmd("upload", root, "<file>")}) and use the url it prints.`;
6792
6851
  } else if (ref.startsWith("data:")) {
6852
+ if (mime.startsWith("audio/")) return `assets/${id}: the player plays no audio stored inside the film's data (a data: ref). Upload the file (${cmd("upload", root, "<file>")}) and use the url it prints.`;
6793
6853
  if (!DATA_URI.test(ref)) return `assets/${id}: the data: ref is not data:<image, font, video or audio type>;base64,\u2026; the player drops it.`;
6794
6854
  if (!ref.startsWith(`data:${mime};`)) return `assets/${id}: mime ${mime} does not match the data: URI's own type (${ref.slice(5, ref.indexOf(";"))}).`;
6795
6855
  } else return `assets/${id}: ref must be this film's own /_blob/<32 hex> (from upload) or a small data:\u2026;base64, URI; never an external URL.`;
@@ -7235,6 +7295,21 @@ function check(root) {
7235
7295
  let seconds = 0;
7236
7296
  const missing = [];
7237
7297
  sceneDurs = /* @__PURE__ */ new Map();
7298
+ const listIds = new Set(list.flatMap((s) => isObject7(s) && typeof s.id === "string" ? [s.id] : []));
7299
+ const renamed = /* @__PURE__ */ new Set();
7300
+ for (const [name, text] of texts) {
7301
+ const m = /^(.+)\.js$/.exec(name);
7302
+ if (!m || !listIds.has(m[1]) || texts.has(`scene-${m[1]}.js`) || ["shared", "score", "narration"].includes(m[1])) continue;
7303
+ if (new RegExp(`\\bkit\\.scene\\s*\\(\\s*['"\`]${m[1].replace(/[^\w-]/g, "")}['"\`]`).test(text)) {
7304
+ renamed.add(m[1]);
7305
+ warn(`modules/${name}`, `the player runs it as scene "${m[1]}" but a scene's code belongs in modules/scene-${m[1]}.js; rename the file.`);
7306
+ }
7307
+ }
7308
+ for (const name of texts.keys()) {
7309
+ const scene = /^scene-(.+)\.js$/.exec(name);
7310
+ if (scene && !listIds.has(scene[1])) warn(`modules/${name}`, `scene-<id>.js is for a scene of scenes.json, and "${scene[1]}" is not one; the player runs no other module so.`);
7311
+ if (/^layer-/.test(name)) warn(`modules/${name}`, "names that begin layer- are the player's; name no module so.");
7312
+ }
7238
7313
  list.forEach((s, i) => {
7239
7314
  const at = `${label} scenes[${i}]`;
7240
7315
  if (!isObject7(s)) return problem(at, "is not an object.");
@@ -7247,7 +7322,7 @@ function check(root) {
7247
7322
  seconds += s.dur;
7248
7323
  if (typeof s.id === "string") sceneDurs.set(s.id, s.dur);
7249
7324
  }
7250
- if (typeof s.id === "string" && !texts.has(`scene-${s.id}.js`)) missing.push(`${i + 1} (${s.id})`);
7325
+ if (typeof s.id === "string" && !texts.has(`scene-${s.id}.js`) && !renamed.has(s.id)) missing.push(`${i + 1} (${s.id})`);
7251
7326
  });
7252
7327
  if (seconds > MAX_SECONDS) problem(label, `the scenes add up to ${seconds} s; a film over ${MAX_SECONDS} s won't load.`);
7253
7328
  scenesInfo = { stage: scenes.stage, scenes: list.length, seconds: Math.round(seconds * 1e3) / 1e3, ...missing.length ? { scenesLeft: missing } : {} };
@@ -7352,7 +7427,10 @@ function check(root) {
7352
7427
  const blob = BLOB_URL.exec(String(data.ref ?? ""));
7353
7428
  if (!blob) continue;
7354
7429
  const meta2 = readBlobMeta(root, blob[1]);
7355
- if (meta2 && meta2.contentType !== data.mime) warn(p, `mime is ${data.mime}, but the upload is ${meta2.contentType}; the player trusts mime.`);
7430
+ if (meta2 && meta2.contentType !== data.mime) {
7431
+ if (String(data.mime).startsWith("audio/") || String(meta2.contentType).startsWith("audio/")) problem(p, `mime is ${data.mime}, but the upload is ${meta2.contentType}; the player drops an audio asset whose mime and upload type differ. Use the upload reply's content_type, verbatim.`);
7432
+ else warn(p, `mime is ${data.mime}, but the upload is ${meta2.contentType}; the player trusts mime.`);
7433
+ }
7356
7434
  }
7357
7435
  const result = {
7358
7436
  root,
@@ -7598,7 +7676,7 @@ async function serve(root, flags) {
7598
7676
  print({ url: state.url, root, pid: state.pid, ...state.lan?.length ? { lan: state.lan } : {} }, true);
7599
7677
  return !(state.joined && background);
7600
7678
  }
7601
- function mountSite(root, { log: log3, network, agentHook }) {
7679
+ function mountSite(root, { log: log3, network, agentHook, local, agentName }) {
7602
7680
  const dbDir = path18.join(root, "db");
7603
7681
  fs19.mkdirSync(dbDir, { recursive: true });
7604
7682
  const known = /* @__PURE__ */ new Map();
@@ -7687,6 +7765,8 @@ data: ${JSON.stringify(data)}
7687
7765
  title: () => filmTitle(root),
7688
7766
  log: log3,
7689
7767
  uiFile: path18.join(EDITOR_DIR, "share-ui.js"),
7768
+ ...local ? { local } : {},
7769
+ ...agentName ? { agentName } : {},
7690
7770
  globals: (viewer, query) => ({ __baoyuHistory: history.bootGlobal(viewer, query) })
7691
7771
  });
7692
7772
  const comments = createCommentsService({
@@ -7838,7 +7918,7 @@ data: ${JSON.stringify(data)}
7838
7918
  let write = { op: body.op, path: body.path, ...body.op === "delete" ? {} : { data: body.data } };
7839
7919
  let merge;
7840
7920
  const base2 = isObject7(body.base) ? body.base : body.base === null && body.op !== "delete" ? {} : null;
7841
- if ((body.op === "set" || body.op === "update" || body.op === "delete") && base2 && (body.op === "delete" || isObject7(body.data))) {
7921
+ if ((body.op === "set" || body.op === "update" || body.op === "delete") && base2 && !REPLACE_ONLY_DOCS.has(segs.join("/")) && (body.op === "delete" || isObject7(body.data))) {
7842
7922
  let now = null;
7843
7923
  try {
7844
7924
  now = readDoc(root, segs.join("/"));
@@ -8381,7 +8461,7 @@ ${USAGE}
8381
8461
  return 2;
8382
8462
  }
8383
8463
  }
8384
- main().then(
8464
+ if (typeof __BAOBOT_LIB__ === "undefined") main().then(
8385
8465
  (code) => {
8386
8466
  if (code >= 0) process.exitCode = code;
8387
8467
  },
@@ -8391,3 +8471,10 @@ main().then(
8391
8471
  process.exitCode = 2;
8392
8472
  }
8393
8473
  );
8474
+ export {
8475
+ applyWrites,
8476
+ check,
8477
+ mountSite,
8478
+ readDb,
8479
+ upload
8480
+ };