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.
- package/lib/index.js +1 -1
- package/package.json +14 -14
- package/skills/claude-motion/SKILL.md +138 -108
- package/skills/claude-motion/artifact-type/reference/by-hand.md +23 -9
- package/skills/claude-motion/artifact-type/reference/design-systems.md +50 -0
- package/skills/claude-motion/artifact-type/reference/film-api.md +129 -252
- package/skills/claude-motion/artifact-type/reference/film-folder.md +107 -0
- package/skills/claude-motion/artifact-type/reference/finalize.md +118 -203
- package/skills/claude-motion/artifact-type/reference/kinds.md +43 -0
- package/skills/claude-motion/artifact-type/reference/later.md +130 -0
- package/skills/claude-motion/artifact-type/reference/motion.md +43 -239
- package/skills/claude-motion/artifact-type/reference/plan.md +4 -4
- package/skills/claude-motion/artifact-type/reference/questions.md +91 -152
- package/skills/claude-motion/artifact-type/reference/recording.md +186 -0
- package/skills/claude-motion/artifact-type/reference/recreate-ui.md +128 -3
- package/skills/claude-motion/artifact-type/reference/render-offline.md +3 -1
- package/skills/claude-motion/artifact-type/reference/rough-cut.md +109 -213
- package/skills/claude-motion/artifact-type/reference/sound.md +49 -100
- package/skills/claude-motion/artifact-type/reference/voice.md +63 -0
- package/skills/claude-motion/artifact-type/reference/walkthrough.md +4 -4
- package/skills/claude-motion/scripts/artifact.mjs +96 -9
- package/skills/claude-motion/scripts/editor/artifact-type/app.css +1 -1
- package/skills/claude-motion/scripts/editor/artifact-type/app.js +1177 -823
- package/skills/claude-motion/scripts/editor/index.html +1 -1
- package/skills/claude-motion/scripts/editor/local-host.js +6 -6
- package/skills/claude-motion/scripts/editor/share-ui.js +10 -10
|
@@ -1,106 +1,55 @@
|
|
|
1
|
-
# Sound
|
|
2
|
-
|
|
3
|
-
The score is
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
- **
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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,
|
|
54
|
-
const
|
|
55
|
-
S.tempo(
|
|
56
|
-
S.program('
|
|
57
|
-
S.note('pad', 'D3', {
|
|
58
|
-
S.chord('
|
|
59
|
-
S.
|
|
60
|
-
S.
|
|
61
|
-
S.
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
`
|
|
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
|
|
96
|
-
|
|
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
|
|
51
|
-
color (never touching the accent) whose
|
|
52
|
-
that
|
|
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
|
|
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 (
|
|
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 =
|
|
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)
|
|
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
|
+
};
|