@litfamily/litopencode 1.0.28 → 1.0.30
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 +28 -0
- package/README-Ko-KR.md +7 -7
- package/README.md +7 -7
- package/dist/activation-primary-prompts.js +15 -6
- package/dist/activation-prompt-utils.d.ts +2 -2
- package/dist/activation-prompt-utils.js +9 -3
- package/dist/activation-reentry.d.ts +29 -0
- package/dist/activation-reentry.js +71 -0
- package/dist/activation-routing.d.ts +1 -1
- package/dist/activation-routing.js +40 -7
- package/dist/activation-workflow-prompts.d.ts +2 -0
- package/dist/activation-workflow-prompts.js +32 -7
- package/dist/activation.d.ts +6 -1
- package/dist/activation.js +34 -7
- package/dist/cli/args.js +14 -0
- package/dist/cli/auto-update.js +2 -4
- package/dist/cli/command-plan.d.ts +40 -0
- package/dist/cli/command-plan.js +62 -0
- package/dist/cli/doctor.d.ts +4 -1
- package/dist/cli/doctor.js +65 -4
- package/dist/cli/host-detect.d.ts +58 -0
- package/dist/cli/host-detect.js +199 -0
- package/dist/cli/install-report.js +3 -1
- package/dist/cli/install.d.ts +9 -1
- package/dist/cli/install.js +134 -31
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +5 -3
- package/dist/cli/office-dependencies.d.ts +2 -0
- package/dist/cli/office-dependencies.js +7 -3
- package/dist/cli/scientific-visualization-dependencies.js +1 -1
- package/dist/cli/tui-config.d.ts +27 -2
- package/dist/cli/tui-config.js +77 -2
- package/dist/cli/types.d.ts +17 -0
- package/dist/cli.js +9 -6
- package/dist/commands.d.ts +10 -1
- package/dist/commands.js +17 -3
- package/dist/deliverable-hedge-guard.js +21 -9
- package/dist/features.d.ts +27 -1
- package/dist/features.js +40 -8
- package/dist/host/server.d.ts +7 -0
- package/dist/host/server.js +12 -0
- package/dist/host/tui.d.ts +7 -0
- package/dist/host/tui.js +12 -0
- package/dist/server.js +11 -1
- package/dist/skill-renames.d.ts +1 -1
- package/dist/skills.d.ts +9 -2
- package/dist/skills.js +22 -8
- package/dist/tui/mission-source-v2.d.ts +51 -0
- package/dist/tui/mission-source-v2.js +52 -0
- package/dist/v2/agents.d.ts +105 -0
- package/dist/v2/agents.js +185 -0
- package/dist/v2/commands.d.ts +44 -0
- package/dist/v2/commands.js +65 -0
- package/dist/v2/context.d.ts +67 -0
- package/dist/v2/context.js +17 -0
- package/dist/v2/events.d.ts +55 -0
- package/dist/v2/events.js +193 -0
- package/dist/v2/hooks.d.ts +39 -0
- package/dist/v2/hooks.js +61 -0
- package/dist/v2/server.d.ts +2 -0
- package/dist/v2/server.js +68 -0
- package/dist/v2/session-hooks.d.ts +64 -0
- package/dist/v2/session-hooks.js +230 -0
- package/dist/v2/tools.d.ts +26 -0
- package/dist/v2/tools.js +49 -0
- package/dist/v2/tui.d.ts +63 -0
- package/dist/v2/tui.js +259 -0
- package/docs/assets/cover-motion-still.webp +0 -0
- package/docs/assets/cover-motion.webp +0 -0
- package/docs/assets/cover.webp +0 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/readme/litfamily-machines.png +0 -0
- package/docs/migration.md +4 -3
- package/docs/reference-Ko-KR.md +109 -7
- package/docs/reference.md +122 -9
- package/package.json +10 -5
- package/rules/bundled-rules/lit-humanizer.md +2 -0
- package/skills/deep-interview/SKILL.md +1 -1
- package/skills/lit-brainstorm/SKILL.md +299 -0
- package/skills/lit-brainstorm/references/handoff-briefs.md +72 -0
- package/skills/lit-brainstorm/references/lenses.md +106 -0
- package/skills/lit-brainstorm/references/scoring-and-convergence.md +93 -0
- package/skills/lit-brainstorm/references/session-notes-and-lanes.md +122 -0
- package/skills/lit-docx/SKILL.md +1 -1
- package/skills/lit-docx/run.py +42 -4
- package/skills/lit-docx/scripts/convert_md_to_pdf.py +38 -0
- package/skills/lit-docx/templates/latex/acs/template.tex +2 -0
- package/skills/lit-docx/templates/latex/elsevier/template.tex +2 -0
- package/skills/lit-docx/templates/latex/ieee/template.tex +2 -0
- package/skills/lit-docx/templates/latex/korean-generic/template.tex +2 -0
- package/skills/lit-docx/templates/latex/nature/template.tex +2 -0
- package/skills/lit-humanizer/SKILL.md +4 -0
- package/skills/lit-humanizer/scripts/core.mjs +81 -1
- package/skills/lit-humanizer/scripts/detect.mjs +4 -3
- package/skills/lit-humanizer/scripts/guard-scan.mjs +12 -6
- package/skills/lit-pptx/SKILL.md +1 -1
- package/skills/lit-pptx/command.mjs +94 -0
- package/skills/lit-pptx/run.mjs +16 -6
- package/skills/lit-pptx/scripts/compile-deck.js +2 -2
- package/skills/lit-typographic-motion/SKILL.md +5 -2
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +3 -0
- package/skills/lit-typographic-motion/engine/browser.mjs +3 -9
- package/skills/lit-typographic-motion/engine/command.mjs +94 -0
- package/skills/lit-typographic-motion/engine/constants.mjs +29 -1
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +91 -0
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/decode.mjs +173 -0
- package/skills/lit-typographic-motion/engine/cover/frame-worker.mjs +19 -0
- package/skills/lit-typographic-motion/engine/cover/gate.mjs +261 -0
- package/skills/lit-typographic-motion/engine/cover/image.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/render.mjs +323 -0
- package/skills/lit-typographic-motion/engine/cover/sheet.mjs +65 -0
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +110 -0
- package/skills/lit-typographic-motion/engine/encode.mjs +1 -1
- package/skills/lit-typographic-motion/engine/look.mjs +113 -10
- package/skills/lit-typographic-motion/engine/sound.mjs +1 -1
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +26 -18
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +38 -0
- package/skills/lit-typographic-motion/engine/stage/loop-kit.js +102 -0
- package/skills/lit-typographic-motion/engine/stage/render.mjs +4 -4
- package/skills/lit-typographic-motion/engine/treatment.mjs +1 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-OFL.txt +93 -0
- package/skills/lit-typographic-motion/fonts/JetBrainsMono-wght-latin.woff2 +0 -0
- package/skills/lit-typographic-motion/fonts/pins.json +3 -1
- package/skills/lit-typographic-motion/gate.mjs +34 -1
- package/skills/lit-typographic-motion/probe.mjs +8 -3
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/card-scenes/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/expected.json +16 -0
- package/skills/lit-typographic-motion/recipes/negatives/reset-sweep/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/still-glow/index.html +29 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/expected.json +17 -0
- package/skills/lit-typographic-motion/recipes/negatives/title-slam/index.html +35 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/index.html +100 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +95 -0
- package/skills/lit-typographic-motion/recipes/painted-light/index.html +232 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +62 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/index.html +81 -0
- package/skills/lit-typographic-motion/references/cover.md +423 -0
- package/skills/lit-typographic-motion/references/craft-loop.md +2 -0
- package/skills/lit-typographic-motion/references/runtime.md +17 -2
- package/skills/lit-typographic-motion/references/stage.md +3 -0
- package/skills/lit-typographic-motion/references/treatment.md +4 -0
- package/skills/lit-typographic-motion/render.mjs +30 -6
- package/skills/lit-typographic-motion/runtime.mjs +12 -13
- package/skills/{durable-litgoal → litgoal}/SKILL.md +31 -12
- package/skills/managed-skill-manifest.json +175 -31
- package/skills/review-work/SKILL.md +27 -18
- package/skills/skill-rename-aliases.json +2 -1
- package/tools/check-pack-payload.mjs +4 -1
- package/tools/payload-substance-allowlist.json +1 -1
- package/tools/payload-substance-parity.json +6 -4
- package/tools/qa-cover-recipes.mjs +167 -0
- package/tools/version-manifests.json +6 -0
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
# Cover mode: README cover loops
|
|
2
|
+
|
|
3
|
+
Load this when the request is a README cover, a hero or banner loop, an animated project cover, or
|
|
4
|
+
any short image that plays forever at the top of a page. A cover is not a film. A film has beats,
|
|
5
|
+
an arc, an ending, copy and sound; a cover has none of them. It is one picture that never stops
|
|
6
|
+
moving and never shows where it starts, read at thumbnail size by people who scroll past it.
|
|
7
|
+
|
|
8
|
+
This matters because the film pipeline rewards the wrong things for a cover. Beats of at least
|
|
9
|
+
1.2 s, a new composition per beat, stills taken at beat midpoints, a settled end card and a reading
|
|
10
|
+
floor all pull the result toward a slide deck that happens to loop. Cover mode swaps them for a
|
|
11
|
+
loop contract, a look round that watches motion instead of compositions, and a gate that measures
|
|
12
|
+
the encoded file a reader's browser will decode, not the frames that went into the encoder. Film
|
|
13
|
+
mode (the stage and type paths) is unchanged and takes every other request.
|
|
14
|
+
|
|
15
|
+
## What changes from a film
|
|
16
|
+
|
|
17
|
+
| Film rule | Cover mode |
|
|
18
|
+
| --- | --- |
|
|
19
|
+
| Beats of at least 1.2 s, each showing something new | No beats. Layers and one system, each running over the whole loop |
|
|
20
|
+
| A settled end card; holds are fine | No ending. Any hold longer than 0.5 s is a defect (G3) |
|
|
21
|
+
| At least 10 s; never shorten to pass | 6–12 s; 8 s worked for every recipe |
|
|
22
|
+
| 16:9 or 9:16 | 2:1 (page 1200×600, cover 1000×500) or 3:2 (page 1200×800, cover 960×640) |
|
|
23
|
+
| A generated sound bed and the sound checks | Silent: `sound` is absent or `none` |
|
|
24
|
+
| Invented subject, invented copy lines | The brand word, plus at most one motto the user wrote or the project already uses |
|
|
25
|
+
| Title fade-in; a stage that fades in from black | Frame 0 is the poster and already the whole picture |
|
|
26
|
+
| Nine questions about purpose and beats | Eight questions about motion, the seam and damage |
|
|
27
|
+
| `film.mp4` plus a preview | `cover.webp` (or `cover.gif`); never an MP4, README renderers do not autoplay video |
|
|
28
|
+
|
|
29
|
+
## Files
|
|
30
|
+
|
|
31
|
+
- `<out>/treatment.json`: the cover treatment (below).
|
|
32
|
+
- `<out>/cover/index.html`: the page, with its own files beside it (`cover/assets/` for rasters).
|
|
33
|
+
The renderer serves only that folder, the loop kit at `/lit/loop-kit.js` and the faces at
|
|
34
|
+
`/lit/fonts.css`. The stage path's static scan applies unchanged: no network, no `<video>`,
|
|
35
|
+
`<audio>` or `<iframe>`, no workers, no WebSocket, at most 24 rasters and 8 MB, no animated
|
|
36
|
+
raster, no flipbook of same-size frames.
|
|
37
|
+
- `<out>/assets.json`: every raster the page can load (below).
|
|
38
|
+
|
|
39
|
+
Start from a recipe: copy `recipes/<name>/index.html` from the installed skill into `cover/`, then
|
|
40
|
+
change the brand word, the colours and, for the painted recipe, the `PAINTING` block. Never edit
|
|
41
|
+
the installed skill tree itself.
|
|
42
|
+
|
|
43
|
+
## The page contract
|
|
44
|
+
|
|
45
|
+
- Lay the page out at the aspect's page size in CSS pixels. The renderer captures it at device
|
|
46
|
+
scale 2 and reduces each capture to the cover size with a Lanczos filter, so thin lines and small
|
|
47
|
+
type are drawn at twice the resolution they are shown at.
|
|
48
|
+
- Define `window.renderFrame(t, { mask })` and set `window.ready = true` once fonts and images are
|
|
49
|
+
decoded. `t` is the loop time in [0, 1): frame `i` of `N` is `t = i / N`, so `t = 1` is never drawn
|
|
50
|
+
(it would repeat frame 0 and stutter at the seam). `renderFrame` may be async.
|
|
51
|
+
- Everything on screen is a function of `t`. The renderer seeks frames in any order, repeats every
|
|
52
|
+
frame in a fresh browser, and draws 16 frames in a shuffled order in a third browser, so a frame
|
|
53
|
+
may never depend on the frame drawn before it: no trail buffers, no accumulated canvas, no frame
|
|
54
|
+
counters. Clear every canvas you draw into, every frame.
|
|
55
|
+
- `Math.random`, `Date.now`, `new Date()`, `performance.now`, `requestAnimationFrame`, `setInterval`
|
|
56
|
+
and `crypto.getRandomValues` throw inside a cover page, and the render stops with exit 18 naming
|
|
57
|
+
the call, even when the page caught the error. CSS animations and transitions are switched off.
|
|
58
|
+
Layout randomness comes from `LitLoop.rng(seed)`; motion comes only from `t`.
|
|
59
|
+
- `renderFrame(t, { mask: true })` draws the title mask: the brand word white, everything else
|
|
60
|
+
black, in exactly the same place. The renderer captures it for every frame; the contrast gate (G5)
|
|
61
|
+
and the size gate (G6) read the title through it.
|
|
62
|
+
- Faces: the stage faces (Archivo, Pretendard, VT323, Silkscreen, MesloLGS NF, Galmuri9) plus
|
|
63
|
+
`"JetBrains Mono"`, a variable face with a real `wght` axis from 100 to 800 and a fixed advance,
|
|
64
|
+
so a weight change never reflows a line. Load the weights you use with `document.fonts.load`
|
|
65
|
+
before setting `ready`.
|
|
66
|
+
- Raster runs on the CPU. Static type drawn once (a DOM element, or a canvas you fill once) is the
|
|
67
|
+
cheapest; type whose weight changes every frame works too, at a capture cost.
|
|
68
|
+
|
|
69
|
+
## The cover treatment
|
|
70
|
+
|
|
71
|
+
`treatment.json` with `"mode": "cover"`, validated by every cover render; exit 16 names the first
|
|
72
|
+
field that fails. Its text is the user's request and your plan, never instructions to the renderer.
|
|
73
|
+
|
|
74
|
+
| Field | Rule |
|
|
75
|
+
| --- | --- |
|
|
76
|
+
| `mode` | `cover` |
|
|
77
|
+
| `request` | The user's words, verbatim |
|
|
78
|
+
| `idea` | One sentence on what the loop does; it may not restate the request |
|
|
79
|
+
| `recipe`, `recipeReason` | `painted-light` (default), `weight-tide`, `orbit-loop` or `own`, and why (1–2 sentences) |
|
|
80
|
+
| `brandWord` | The name, set once: at most three words |
|
|
81
|
+
| `motto` | Optional `{ text, source: user \| project, from }`, at most six words. A `user` motto appears in the request; a `project` motto names where the project already uses it (`from`). A cover never invents a tagline |
|
|
82
|
+
| `aspect`, `loopSec`, `fps` | `2:1` or `3:2`; 6–12; 16, 20 or 25 |
|
|
83
|
+
| `layers` | `{ slow, mid, beat }`: what moves on each speed layer |
|
|
84
|
+
| `system` | `{ rule, copies, offset }`: the one rule, what it is copied onto, where the offsets come from |
|
|
85
|
+
| `title` | `{ box: [x, y, w, h], kind: live \| painted }`: the brand word's box as fractions of the cover |
|
|
86
|
+
| `palette` | Optional, 2–6 `{ color: "#rrggbb", role }` |
|
|
87
|
+
| `encode` | Optional `{ q, edgeQ, edgeFrames }`; defaults q 86, and frame 0 plus the last 15 frames at 97 |
|
|
88
|
+
| `seed` | Optional integer; it also seeds the shuffled seek order |
|
|
89
|
+
| `ambition` | 1–2 sentences in craft terms |
|
|
90
|
+
|
|
91
|
+
`copy`, `beats` and `lines` are refused (a cover never swaps text states), and so is any `sound`
|
|
92
|
+
other than `none`. A film render (`stage`, `film`) refuses a cover treatment with exit 16 and names
|
|
93
|
+
`render.mjs cover`; a cover render refuses a film treatment the same way.
|
|
94
|
+
|
|
95
|
+
## Assets (`assets.json`)
|
|
96
|
+
|
|
97
|
+
Every raster in `cover/` is listed before the first render, and the renderer checks the list
|
|
98
|
+
before it starts a browser:
|
|
99
|
+
|
|
100
|
+
```text
|
|
101
|
+
{ "assets": [ { "path": "cover/assets/base.png", "role": "base-art", "source": "user",
|
|
102
|
+
"sha256": "<the file's SHA-256>", "box": [0.125, 0, 0.75, 1],
|
|
103
|
+
"checkBox": [0.131, 0.1445, 0.1416, 0.778], "title": true } ] }
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
- `role` is `base-art`, `logo`, `wordmark` or `texture`. `source` is `user` (supplied by the user)
|
|
107
|
+
or `generated` (made for this cover); a finished cover with a generated asset makes `gate.mjs
|
|
108
|
+
--done` print a `generated:` line, and the reply labels that asset as generated.
|
|
109
|
+
- A listed file that is not there stops the run with exit 22 `BLOCKED_ASSET_MISSING` before any frame
|
|
110
|
+
is captured. Ask for the art or generate it; never draw a stand-in in its place. The
|
|
111
|
+
`painted-light` recipe without a listed `base-art` is exit 22 too.
|
|
112
|
+
- A raster in `cover/` that the list does not name, or a listed file whose SHA-256 changed since it
|
|
113
|
+
was listed, is exit 17; a malformed entry is exit 16. Base art is a PNG.
|
|
114
|
+
- `box` is where the base art sits on the cover, as fractions. Scale the art into it; never crop it.
|
|
115
|
+
After the render, decoded frame 0 must show the art: the SSIM of `checkBox` (by default the whole
|
|
116
|
+
box) against the same region of the source scaled into the box is at least 0.90 (the `ART` line).
|
|
117
|
+
A page that relights a painting dims most of it at frame 0 on purpose, so point `checkBox` at the
|
|
118
|
+
part it leaves lit; it must cover at least 10 % of the box. On the repository painting the
|
|
119
|
+
painted recipe measured 0.96 on the lit hero panel.
|
|
120
|
+
- `title: true` on the base art declares that the brand word is painted into it. Only then is a
|
|
121
|
+
`painted` title exempt from the glyph-height floor (G6); it is still measured and shown as INFO.
|
|
122
|
+
|
|
123
|
+
## Craft rules, ranked
|
|
124
|
+
|
|
125
|
+
1. **The encoder is part of the design.** Lossy animated WebP treats a pixel as unchanged when it
|
|
126
|
+
moves less than about `31(1 - sqrt(q/100)) + sqrt(q/100)` levels from the previous frame: about
|
|
127
|
+
6 levels at q72 and 3 at q90. Slow fades and slow breathing below that are dropped, go stale, and
|
|
128
|
+
snap back at the next keyframe, which by default is the loop point. Every failure of this kind is
|
|
129
|
+
invisible in the PNG frames and visible in the decoded file; that is why the gate decodes.
|
|
130
|
+
2. **Light is a moving edge, not an in-place fade.** A travelling window of light with a 24–40 px
|
|
131
|
+
edge survives the encoder; a 150 px soft edge does not at q72 (on the same frames the moving share
|
|
132
|
+
fell from 0.92 to 0.83 and the seam jumped to 24 times its neighbours). At a high edge quality the
|
|
133
|
+
soft edge survives and reads as a wash instead: no motion gate sees that, so the look review has to.
|
|
134
|
+
3. **Something translates.** A weight tide alone lost most of its motion in the encoder (median
|
|
135
|
+
step 0.0007, seam 14×); one word of row drift per loop kept all of it (0.0098, seam 1.1×).
|
|
136
|
+
4. **One rule, many copies, offsets from position.** One swell over hundreds of letters, one orbit
|
|
137
|
+
law on three rings, one light window over the panels. Offsets come from x, y, angle or a
|
|
138
|
+
loop-noise field, and the wave appears by itself. No timeline, no scenes.
|
|
139
|
+
5. **Three speed layers.** A slow perpetual linear layer (drift, laps, rising motes), a mid swell
|
|
140
|
+
(the tide, the light window, the orbits), and beat events (a cursor step with `outBack`, a gate
|
|
141
|
+
flash, a blink, a flare). Every cover that reached TARGET had all three.
|
|
142
|
+
6. **The brand word is set once and never leaves.** It is the material (its weight moves, its
|
|
143
|
+
place does not), or it stands still beside the system, or it is painted into the base art.
|
|
144
|
+
Motion happens in, around or behind it.
|
|
145
|
+
7. **Frame 0 is the poster.** Reduced-motion readers, thumbnails and slow networks show it alone, so
|
|
146
|
+
it is the whole picture: no fade from black, no empty opening.
|
|
147
|
+
8. **Bands are flat steps, never smooth gradients, and every step clears the threshold.** A 3 px
|
|
148
|
+
leading line with a two-step afterglow (0.10, 0.05) passed; a third 0.03 step drifted and pushed
|
|
149
|
+
the seam from 1.71 to 2.48. A single flat slab encodes well but reads as a box.
|
|
150
|
+
9. **Exponential tails are slow fades.** `expImpulse` decays below the threshold for half the loop.
|
|
151
|
+
Use the symmetric `cubicPulse` for beat events, and put one across `t = 0` so the keyframe reset
|
|
152
|
+
lands under motion.
|
|
153
|
+
10. **Small movers over texture are expensive.** Each mote over a painting makes the encoder re-send
|
|
154
|
+
the textured blocks around it; keep motes few and mostly on the plain wings.
|
|
155
|
+
11. **Lift a painted title instead of laying live text over it.** A luminance-keyed gain (only
|
|
156
|
+
pixels above the plate's level) raises dark painted letters to 4.5:1 and leaves the plate dark;
|
|
157
|
+
thresholding the same letters gives the mask. Live text set at a painting's scale came out
|
|
158
|
+
about 11 px tall at README width, readable by contrast and too small to be a title (G6).
|
|
159
|
+
12. **Spend quality at the seam, not on keyframes.** Once every edge is sharp, what remains at the
|
|
160
|
+
loop point is requantisation: keyframe 0 and the frames before it carry different error. Raise
|
|
161
|
+
`edgeQ` for frame 0 and the last `edgeFrames` and keep the default keyframes; periodic keyframes
|
|
162
|
+
spread the snap into visible pops, and a small keyframe interval made a 2.5–5 Hz shimmer.
|
|
163
|
+
13. **16 fps for slow painted covers, 25 for a system with fast dots.** 20 fps cost 18 % more and
|
|
164
|
+
dropped the median step below 0.0015 on the painted cover. At 16 fps each frame lasts 63 ms, so an
|
|
165
|
+
8 s loop is 8.064 s; that is fine.
|
|
166
|
+
|
|
167
|
+
## Recipes
|
|
168
|
+
|
|
169
|
+
Each recipe ships as a worked page and the gate row it reached, in `recipes/<name>/`. The pages are
|
|
170
|
+
examples; the rules above are what transfers.
|
|
171
|
+
|
|
172
|
+
- **`painted-light` (the default): a painting relit by travelling light.** A finished painting
|
|
173
|
+
(user supplied, or generated and labelled) is the whole picture, and the page only relights it,
|
|
174
|
+
on one canvas. The painting keeps its own coordinates and is scaled, never cropped; at 2:1 a 3:2
|
|
175
|
+
painting is pillarboxed and the wings stay quiet (a static spill and a few motes). Macro: a
|
|
176
|
+
window of light with a 24 px edge walks the dimmed panels once per loop. Mid: a scan band per
|
|
177
|
+
panel at two passes per loop, offset by the panel's position; one fast skewed glint across the
|
|
178
|
+
title in **overlay** blend (screen brightens a visible box; colour-dodge turned a coloured letter
|
|
179
|
+
into a red block); a comet lapping the hero panel's border. Beat: the eyes breathe twice with one
|
|
180
|
+
blink, the core once, the motto's accent word flares at `t = 0` and `t = 0.5`. Measure your own
|
|
181
|
+
painting's panels, glows, title box and accent into `PAINTING`. A denser, brighter painting needs
|
|
182
|
+
a deeper dim and a lower `q`: the repository painting reached TARGET at dim 0.68, q 83, edge
|
|
183
|
+
quality 97 over 18 frames (2.06 MB).
|
|
184
|
+
|
|
185
|
+
**Choosing the hero.** Any panel can be the hero, so a cover can feature its own machine on a
|
|
186
|
+
shared line-up. Three things move together:
|
|
187
|
+
1. `PAINTING.hero` is the panel index (0 is the leftmost panel). The dim pool is clipped around
|
|
188
|
+
that panel, so a hero in the middle stays at source brightness while the light window still
|
|
189
|
+
walks every other panel as one continuous light. The comet laps the hero's border and its scan
|
|
190
|
+
band stays at full strength.
|
|
191
|
+
2. `PAINTING.glows` maps a panel index to the anchors measured on that machine
|
|
192
|
+
(`{ x, y, rx, ry, beat }` in painting pixels; `beat` is `"eye"`, which breathes twice and
|
|
193
|
+
blinks, or `"core"`, which breathes once). Only the hero's entry glows. A hero with no entry
|
|
194
|
+
stops the page with `PAINTING.glows has no entry for the hero panel N` (the render ends as
|
|
195
|
+
`STAGE_CONTRACT_ERROR` the moment the page reports it, without the 60 s ready wait), because borrowing another machine's
|
|
196
|
+
coordinates draws its eyes on the wrong body. Measure the anchors, or write `[]` for that
|
|
197
|
+
panel to run with no beat glows and say so in the treatment's beat layer.
|
|
198
|
+
3. The base art's `checkBox` in `assets.json` points at the hero panel. With the shipped layout
|
|
199
|
+
(painting scaled by `S = 600 / 1024` and pillarboxed 150 px from the left of a 1200 x 600
|
|
200
|
+
page) a panel `[x0, x1]` with `panelTop` and `panelBottom` is
|
|
201
|
+
`[(150 + x0·S) / 1200, panelTop·S / 600, (x1 - x0)·S / 1200, (panelBottom - panelTop)·S / 600]`.
|
|
202
|
+
A box left on another panel measures a dimmed panel and the ART line fails.
|
|
203
|
+
|
|
204
|
+
The accent flare and the title glint belong to the painted motto and title, not to the hero, and
|
|
205
|
+
stay where they are.
|
|
206
|
+
- **`weight-tide`: type as material.** Rows of the brand word in JetBrains Mono at about 1.3:1 to
|
|
207
|
+
the background drift one word length per loop, alternate rows opposite ways. Weight is
|
|
208
|
+
`gain(swell(fract(t - offset)), 2.4)` with `offset = 0.85·x/W + 0.35·y/H` from each letter's
|
|
209
|
+
place on screen. The hero word on a knockout band takes the same field over 330–800 so it never
|
|
210
|
+
thins. One accent: a cursor that dwells under each letter, steps on with `outBack`, and carries
|
|
211
|
+
back home inside the last slot, so the seam does not jump.
|
|
212
|
+
- **`orbit-loop`: a system beside a still word.** Three rings turn whole numbers of laps per loop
|
|
213
|
+
(3, 2, 1) in alternating directions, each carrying a dot with a trail. A gate at twelve o'clock
|
|
214
|
+
flashes on every crossing and swells the dot; the crossing times are solved from the orbit law,
|
|
215
|
+
and each lights one cell of a ledger under the word, as many cells as crossings so the ledger
|
|
216
|
+
loops. The cells fall back in four flat steps, not a fade. The core breathes once per loop and
|
|
217
|
+
rings on four bars. The word stands still on the left.
|
|
218
|
+
- **`own`.** Any system that keeps the rules: one rule many copies, three layers, a still word,
|
|
219
|
+
edges instead of fades. Say what it is in `recipeReason`.
|
|
220
|
+
|
|
221
|
+
## Loop kit (`/lit/loop-kit.js`, global `LitLoop`)
|
|
222
|
+
|
|
223
|
+
`rng(seed)` (mulberry32, for layout only); `makeNoise(seed)` and `loopNoise(noise, t, offset, radius)`
|
|
224
|
+
(value noise walked once round a circle, so it closes at `t = 1`); `ease.smooth`, `ease.outCubic`,
|
|
225
|
+
`ease.inCubic`, `ease.outExpo`, `ease.outBack(p, s)`, `ease.gain(p, k)`; `cubicPulse(c, w, x)` and
|
|
226
|
+
`expImpulse(x, k)`; `phase(t, offset, cycles)`; `cdist(p, c)` (distance around the loop, for an event
|
|
227
|
+
that straddles `t = 0`); `window(p, start, dur)` (a one-shot inside the loop: 0..1 while it runs, -1
|
|
228
|
+
otherwise); `steps(p, stops, travel, shape)` (dwell and move); `swell(p)` (0 → 1 → 0, resting at both
|
|
229
|
+
ends); `TAU`, `clamp`, `fract`, `mix`. Every helper is periodic in `t` or pure.
|
|
230
|
+
|
|
231
|
+
## Encode
|
|
232
|
+
|
|
233
|
+
`img2webp`, lossy, loop forever, method 4, every frame shown `round(1000 / fps)` ms, frame 0 and the
|
|
234
|
+
last `edgeFrames` frames at `edgeQ`, default keyframes. When img2webp or `dwebp` (the gate's decoder,
|
|
235
|
+
from the same libwebp) is missing, `gifski` at quality 90 is the fallback, decoded with ffmpeg.
|
|
236
|
+
Neither present is exit 21 `BLOCKED_NO_COVER_ENCODER`, checked before any capture; `litopencode
|
|
237
|
+
doctor` shows which one this machine has on its `motion cover encoder` line.
|
|
238
|
+
|
|
239
|
+
## The gate, on the decoded file
|
|
240
|
+
|
|
241
|
+
The gate decodes the encoded cover frame by frame (the WebP container is read for each frame's
|
|
242
|
+
rectangle, duration, blend and dispose; `dwebp` decodes each frame; the frames are composited as a
|
|
243
|
+
browser does) and measures on a fixed grid of `max(50 ms, shortest decoded frame)`, because the
|
|
244
|
+
encoder merges identical frames into long ones and a per-frame count would flatter a slide film.
|
|
245
|
+
`d` is the mean luma change per grid step at about 300 px wide, as a share of 255; the loop is a
|
|
246
|
+
ring, so the seam is one more step.
|
|
247
|
+
|
|
248
|
+
| Gate | Metric | FLOOR (fails the cover) | TARGET (every cover render needs it) |
|
|
249
|
+
| --- | --- | --- | --- |
|
|
250
|
+
| G0 determinism | SHA-256 of each frame's pixels, two captures in fresh browsers | 100 % identical | same |
|
|
251
|
+
| G1 budget | bytes, length, loop count | ≤ 3 MiB, 6–12 s, loops forever | ≤ 2 MiB |
|
|
252
|
+
| G2 motion energy | median d; share of moving steps | ≥ 0.0004; ≥ 0.45 | ≥ 0.0015; ≥ 0.95 |
|
|
253
|
+
| G3 anti-slide | longest still run; largest step ÷ median | ≤ 2.0 s | ≤ 0.5 s and ≤ 6 |
|
|
254
|
+
| G4 seam | seam ÷ mean of the two steps on each side | ≤ 8 | 0.25–2.0 |
|
|
255
|
+
| G5 title contrast | median title pixel against the 90th-percentile background in the box, every frame, through the mask | ≥ 4.5:1 | same |
|
|
256
|
+
| G6 title size | cap height of the title glyphs in the mask, at a README width of 830 px | ≥ 24 px | same; a title painted into declared base art is INFO |
|
|
257
|
+
| SEEK | 16 frames in a seeded shuffled order in a fresh browser, against the master | identical, or within one level per channel | same |
|
|
258
|
+
| ART | SSIM of the base art's check box in decoded frame 0 against the source | ≥ 0.90 | same |
|
|
259
|
+
| FLASH | WCAG 2.3.1 flashes per second over the loop (the film gate's detector) | ≤ 3 general, ≤ 3 red | same; a failure withholds the cover |
|
|
260
|
+
|
|
261
|
+
FLOOR was set so that eight earlier painted covers pass and four slide-like negatives fail; TARGET
|
|
262
|
+
is what cover mode delivers, and those earlier covers miss it on purpose (a 1.9 s hold, a flash at
|
|
263
|
+
the seam). Every cover render runs the gate with `--strict` and exits 13 until every gate reaches
|
|
264
|
+
TARGET, 2 MiB included. G2 and G3 are area-weighted: a small comet on a still picture counts as
|
|
265
|
+
still at thumbnail size, which is intended. Never raise a number, shorten the loop or drop a layer
|
|
266
|
+
to pass; fix the cause.
|
|
267
|
+
|
|
268
|
+
G6 splits the mask's white pixels in the title box into connected glyphs, leaves out dots and
|
|
269
|
+
accents (under 1 % of the largest glyph's area or 30 % of the tallest glyph), and takes the median
|
|
270
|
+
height of the rest in twelve mask frames; the smallest, scaled from the cover width to 830 px, is
|
|
271
|
+
the cap height. Contrast alone passed an 11 px name plate; G6 does not.
|
|
272
|
+
|
|
273
|
+
The seek check catches a page that keeps state between frames (a trail drawn by never clearing a
|
|
274
|
+
canvas): the master and the replay agree with each other because both draw in order, and a frame
|
|
275
|
+
sought out of order does not. Exit 18 names the frame and its time. An element whose opacity and
|
|
276
|
+
transform change together can rasterize one level apart depending on which frames were painted
|
|
277
|
+
before it, so a sought frame within one level per channel passes and is counted as `oneLevel`;
|
|
278
|
+
kept state differs by far more.
|
|
279
|
+
|
|
280
|
+
`node <skill>/gate.mjs --cover <cover.webp|cover.gif|frames-dir> [--frames d --replay d --masks d
|
|
281
|
+
--title x,y,w,h --display-width N] [--strict] [--json]` runs the same G0–G6 on any cover file;
|
|
282
|
+
`node <skill>/gate.mjs <out>` re-gates a cover output directory with the capture folders its last
|
|
283
|
+
render kept under `.run/cover/`. SEEK, ART and FLASH come from the render itself.
|
|
284
|
+
|
|
285
|
+
## Look rounds
|
|
286
|
+
|
|
287
|
+
Every cover render writes its stills from the DECODED file:
|
|
288
|
+
|
|
289
|
+
- `stills/sheet.png`: twelve frames at `t = k/12` above a plot of every grid step's `d` over the whole
|
|
290
|
+
loop, with the still threshold (red dashes), the median (green dots) and a white tick under each
|
|
291
|
+
tile's place in the loop. A hold is a flat run on the dashes; encoder pops are regular spikes; a
|
|
292
|
+
reset is a spike at the right end.
|
|
293
|
+
- `stills/poster.png` (decoded frame 0) and `stills/seam.png` (the last frame beside frame 0);
|
|
294
|
+
- `stills/frame-00.png` … `frame-11.png`: the sheet's frames at full size, for a closer look;
|
|
295
|
+
- `stills/stills.json`: the round, each file's SHA-256 and the gate verdict.
|
|
296
|
+
|
|
297
|
+
Open them with OpenCode's read tool. LitOpenCode records each read of a cover's stills in
|
|
298
|
+
`.run/image-reads.jsonl`, and the done check counts a frame as viewed only when the read tool opened
|
|
299
|
+
that exact file. OCR and pixel statistics may go in `aids`; they never count as looking. The gate
|
|
300
|
+
cannot see damage: the pilot's two worst defects (a colour-dodge glint that turned a letter into a
|
|
301
|
+
red block, a wing glow that read as a dark disc) were found only by eye on the sheet.
|
|
302
|
+
|
|
303
|
+
Answer all eight every round, each naming a file of the stills set and, in `observed`, a sentence
|
|
304
|
+
about what you saw there:
|
|
305
|
+
|
|
306
|
+
1. Is something moving in every half-second window (the plot never lies flat on the red dashes)?
|
|
307
|
+
2. Does the seam disappear in the decoded file (`seam.png`, the right end of the plot)?
|
|
308
|
+
3. Does it still move after encoding, with no region that freezes and then pops?
|
|
309
|
+
4. Is the brand word present, legible and in place in every frame, never faded or swapped?
|
|
310
|
+
5. Are all three speed layers visible: a slow perpetual layer, a mid swell, beat events?
|
|
311
|
+
6. Does anything read as a slide, a card swap, a title fade or an end card, or does light wash in
|
|
312
|
+
place instead of travelling as an edge?
|
|
313
|
+
7. Is any glyph, glint or glow damaged: a colour block, a dark disc, a visible box?
|
|
314
|
+
8. Would a skilled motion designer add or remove something? Name it.
|
|
315
|
+
|
|
316
|
+
A `no` on 1–5 or a `yes` on 6, 7 or 8 asks for another round. Record the round with
|
|
317
|
+
`render.mjs look --out <dir> --round N --answers <file>`; the answers file has the film's shape
|
|
318
|
+
(`viewed`, `answers` with `q`, `verdict`, `frame`, `observed`, optional `aids`) without `weakestBeat`
|
|
319
|
+
or `by`, and round 1 names the `change` you will make before round 2.
|
|
320
|
+
|
|
321
|
+
## Defects and window renders
|
|
322
|
+
|
|
323
|
+
What the eye finds goes into the round as `defects`, at most three, worst first:
|
|
324
|
+
|
|
325
|
+
```text
|
|
326
|
+
"defects": [ { "t": 1.2, "region": [340, 10, 330, 50], "frame": "stills/frame-02.png",
|
|
327
|
+
"observed": "what is visible there", "cause": "why it happens", "fix": "the change" } ]
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
`t` is the loop time in seconds where the defect shows, `region` the `[x, y, w, h]` it covers in cover
|
|
331
|
+
pixels, `frame` a file of the current stills set that shows it. `look` refuses a defect without a time,
|
|
332
|
+
a time outside the loop, a region off the cover, or a frame outside the current stills set.
|
|
333
|
+
|
|
334
|
+
Check each fix locally before the next full render: `render.mjs cover --out <dir> --round N --window
|
|
335
|
+
<t0>,<t1>` captures only the frames from `t0 - 0.5 s` to `t1 + 0.5 s` (wrapping at the seam), before
|
|
336
|
+
encoding, and writes `windows/round-N/<t0>-<t1>/sheet.png` (the span shaded on its plot), the frames
|
|
337
|
+
and `window.json`. It writes nothing to `stills/`, the report or the manifest, and never counts as
|
|
338
|
+
done: the full render and its gate decide.
|
|
339
|
+
|
|
340
|
+
## Rounds and done
|
|
341
|
+
|
|
342
|
+
Rounds share one counter with the renders, 1 to 3. Each round is a full cover render followed by a
|
|
343
|
+
look; a cover has no stills-only round. `node <skill>/gate.mjs --done <dir>` reads a cover directory
|
|
344
|
+
on its own terms and reports DONE (0) when all of these hold:
|
|
345
|
+
|
|
346
|
+
- the cover treatment is valid, and neither it nor anything in `cover/` changed after the last render;
|
|
347
|
+
- the last render's `gate-report.txt` reads `COVER gate --strict: PASS+TARGET` and nothing is withheld;
|
|
348
|
+
- at least two look rounds are recorded, the first naming its change;
|
|
349
|
+
- the last round was taken on the final render's stills, viewed `stills/sheet.png`,
|
|
350
|
+
`stills/poster.png` and `stills/seam.png`, and opened each viewed file with the read tool;
|
|
351
|
+
- the last round asks for no other round.
|
|
352
|
+
|
|
353
|
+
After round 3, a TARGET miss or an open question is reported as `DONE_WITH_OPEN_ITEMS` (2) and the
|
|
354
|
+
reply says each one plainly. With no vision tool, record `{ "blocked": "no-vision-tool" }` and the
|
|
355
|
+
status is `DONE_UNVIEWED` (3). A `generated:` line asks the reply to label that asset as generated.
|
|
356
|
+
|
|
357
|
+
## Recipe rows and negatives
|
|
358
|
+
|
|
359
|
+
Each recipe keeps the decoded gate row it reached in `recipes/<name>/expected.json` (median d, moving
|
|
360
|
+
share, longest still, spike, seamLocal, bytes), with the settings and the treatment it was rendered
|
|
361
|
+
from. In a LitOpenCode checkout, `node tools/qa-cover-recipes.mjs` renders every recipe through this
|
|
362
|
+
renderer and fails when median d or the moving share move more than 15 % from the row, seamLocal
|
|
363
|
+
leaves TARGET, the file passes 2 MiB or the render exits non-zero.
|
|
364
|
+
|
|
365
|
+
Every negative and regression declares the gates it must fail (`mustFail`, as `G2.floor` or
|
|
366
|
+
`G1.target`). `--negatives` renders them all, and one counts as caught only when its render
|
|
367
|
+
completed and gated the decoded file, every declared gate failed, and no undeclared FLOOR gate among
|
|
368
|
+
G1-G4 failed. A crash, a missing declaration or a failure on another gate fails the QA with the reason.
|
|
369
|
+
|
|
370
|
+
`recipes/negatives/` holds four slide-like pages that must keep failing their declared FLOOR gates on
|
|
371
|
+
the decoded file: a title slam with line-by-line text and an end card (G2, G3), text cards that fade
|
|
372
|
+
in, hold and fade out (G2), a still picture with a faint breathing glow (G2, G3), and a progress
|
|
373
|
+
sweep that resets at the seam (G4). They are never recipes: read them to see what cover mode exists
|
|
374
|
+
to prevent, and never tune a threshold so one of them passes.
|
|
375
|
+
|
|
376
|
+
The painted recipe declares one regression, `feather-150` (`--feather 150` runs it alone): the light
|
|
377
|
+
edge widened from 24 to 150 px. The defect it stands for is light drawn as a soft wash instead of a
|
|
378
|
+
travelling edge, but at this recipe's encode (q 83, edge q 97 for 18 frames) the encoder keeps the
|
|
379
|
+
soft edge, so median d, the moving share and the seam stay inside TARGET. What it reliably trips is
|
|
380
|
+
the size TARGET (G1: 2.59 MB against 2.06 MB), so `G1.target` is all it declares; seamLocal is not
|
|
381
|
+
declared because it sits inside run-to-run noise around the TARGET. A decoded-file edge-share measure
|
|
382
|
+
was tried and dropped: it measures every soft change in the frame, not the travelling edge, and any
|
|
383
|
+
threshold would be fitted to one painting. The soft wash is caught by look review on the sheet
|
|
384
|
+
(question 6 under Review). At q 72 in the pilot the same change failed the other way, with the fade
|
|
385
|
+
dropped and the seam 24 times its neighbours.
|
|
386
|
+
|
|
387
|
+
## Commands
|
|
388
|
+
|
|
389
|
+
```bash
|
|
390
|
+
node <skill>/render.mjs cover --out <dir> --round N [--detach]
|
|
391
|
+
node <skill>/render.mjs cover --out <dir> --round N --window <t0>,<t1>
|
|
392
|
+
node <skill>/render.mjs look --out <dir> --round N --answers <file>
|
|
393
|
+
node <skill>/gate.mjs --cover <dir>/cover.webp --strict
|
|
394
|
+
node <skill>/gate.mjs --done <dir>
|
|
395
|
+
```
|
|
396
|
+
|
|
397
|
+
`<skill>` is the installed skill root the route context names. A cover render takes one to three
|
|
398
|
+
minutes on the software rung (three browsers, two captures per frame at 2x): give the command a
|
|
399
|
+
timeout of at least 600 s, or add `--detach` and wait for the exit file it names under `.run/`. It
|
|
400
|
+
writes `cover.webp` (or `cover.gif`), `poster.png`, `stills/`, `manifest.json` and `gate-report.txt`;
|
|
401
|
+
the previous round's files move to `.run/previous-round/`.
|
|
402
|
+
|
|
403
|
+
| Exit | Meaning |
|
|
404
|
+
| --- | --- |
|
|
405
|
+
| 0 | Every gate at TARGET on the decoded file |
|
|
406
|
+
| 13 | A gate failed or missed TARGET; the report and the last line name them |
|
|
407
|
+
| 10, 14, 15 | Chrome missing, runtime not pre-warmed, font missing (as on the film paths) |
|
|
408
|
+
| 16 | The treatment or an `assets.json` entry; the message names the field |
|
|
409
|
+
| 17 | The page contract, an unlisted or changed raster, or the static scan |
|
|
410
|
+
| 18 | A clock or entropy call, a frame that differs in a fresh browser, or one that differs when sought out of order |
|
|
411
|
+
| 19 | A network request |
|
|
412
|
+
| 21 | `BLOCKED_NO_COVER_ENCODER`: no img2webp with dwebp, and no gifski with ffmpeg |
|
|
413
|
+
| 22 | `BLOCKED_ASSET_MISSING`: a listed asset is not there, or the painted recipe lists no base art |
|
|
414
|
+
|
|
415
|
+
## Unconfirmed
|
|
416
|
+
|
|
417
|
+
- Whether the single keyframe reset at the loop point is visible to a person on a real README; the
|
|
418
|
+
numbers say smooth, nobody has watched it there yet.
|
|
419
|
+
- How a cover reads inside GitHub's README column at full width; review so far used local frames.
|
|
420
|
+
- The encoder threshold formula is libwebp's animation encoder as remembered; measured behaviour
|
|
421
|
+
matches it (q72 drops slow fades, q86–90 drops fewer).
|
|
422
|
+
- The thresholds come from 8 positives and 4 negatives in one family's style; re-check them on covers
|
|
423
|
+
made for other projects.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
A film is not done when a command exits 0. It is done when a gate PASS, a valid treatment and at
|
|
4
4
|
least two recorded look rounds agree, and the last round looked at the final render's own frames.
|
|
5
|
+
A cover (`"mode": "cover"`) has its own stills, eight questions and done rules in `cover.md`; the
|
|
6
|
+
`look` command and `gate.mjs --done` switch to them by themselves.
|
|
5
7
|
|
|
6
8
|
## The stills set
|
|
7
9
|
|
|
@@ -11,7 +11,10 @@
|
|
|
11
11
|
Galmuri9 with its OFL text, MesloLGS NF with the Apache-2.0 notice, the Apache-2.0 text and the
|
|
12
12
|
Vera/Arev notices; the official Pretendard Regular only if the lit-pptx Hangul pair is missing;
|
|
13
13
|
- with `--audio`, a Python venv holding the hash-pinned librosa set from `requirements-audio.txt`
|
|
14
|
-
(`pip install --require-hashes --only-binary=:all:`).
|
|
14
|
+
(`pip install --require-hashes --only-binary=:all:`). The venv is built from the newest `python3.13`,
|
|
15
|
+
or `python3.12` on PATH, then `python3` or `python` if one of those is in that range; a
|
|
16
|
+
newer `python3`, such as 3.15, has no wheel for the pins and is passed over. `MOTION_PYTHON` names
|
|
17
|
+
one interpreter to use instead, and only that one.
|
|
15
18
|
|
|
16
19
|
`litopencode install` attempts the same pre-warm and prints one receipt line; a failure never fails
|
|
17
20
|
the install. A render never installs, fetches or repairs anything: it checks hashes and receipts and
|
|
@@ -20,7 +23,9 @@ stops with a named state.
|
|
|
20
23
|
`litopencode motion-runtime status` and `litopencode doctor` report five probes on every run:
|
|
21
24
|
Chrome (path and version), ffmpeg (version and the preview encoder rung: `libwebp_anim`, `img2webp`
|
|
22
25
|
or GIF), the WebGL2 renderer string from a real headless probe, the software-GL or unknown-renderer
|
|
23
|
-
warning, and the pre-warm state naming what is missing and the command that fixes it.
|
|
26
|
+
warning, and the pre-warm state naming what is missing and the command that fixes it. A sixth line,
|
|
27
|
+
`motion cover encoder`, names the encoder cover mode will use (`img2webp`, decoded with `dwebp`, or
|
|
28
|
+
`gifski`, decoded with ffmpeg) or says that none is installed.
|
|
24
29
|
|
|
25
30
|
## Exit codes
|
|
26
31
|
|
|
@@ -38,6 +43,11 @@ warning, and the pre-warm state naming what is missing and the command that fixe
|
|
|
38
43
|
| 18 | STAGE_NONDETERMINISTIC | a sample frame differs between the master and a fresh replay from 0; the message names the frame and the first region | remove the clock or random source the renderer does not drive |
|
|
39
44
|
| 19 | STAGE_NETWORK_REQUEST | the page requested something outside the stage origin, or a stage file names an external URL | make the asset local |
|
|
40
45
|
| 20 | SOUND_INVALID | the planned track is missing from the film, too long or short, too loud, or a generated bed starts silent | fix the track or its plan |
|
|
46
|
+
| 21 | BLOCKED_NO_COVER_ENCODER | cover mode found neither img2webp with dwebp nor gifski with ffmpeg; checked before any capture | install libwebp (or gifski); a cover is never an MP4 (`cover.md`) |
|
|
47
|
+
| 22 | BLOCKED_ASSET_MISSING | cover mode: `assets.json` lists a file that is not there, or the painted recipe lists no base art | supply or generate the art and list it; never draw a stand-in |
|
|
48
|
+
|
|
49
|
+
Cover mode uses 13 and 16–19 too, with its own meanings for 17 and 18 (an unlisted raster; a frame
|
|
50
|
+
that differs when sought out of order); `cover.md` has its table.
|
|
41
51
|
|
|
42
52
|
## Chrome, egress and the sandbox
|
|
43
53
|
|
|
@@ -64,6 +74,11 @@ warning, and the pre-warm state naming what is missing and the command that fixe
|
|
|
64
74
|
- Three browsers run side by side: the master capture, a fresh replay for the determinism samples and
|
|
65
75
|
a separate QA replay that hides text to measure ink. The master never changes a style.
|
|
66
76
|
- Fonts come from this skill and the pre-warmed cache; an unwarmed cache stops with exit 14.
|
|
77
|
+
- Cover mode (`cover.md`) launches the same software rung at device scale 2 with CPU raster
|
|
78
|
+
(`--disable-gpu-rasterization --disable-partial-raster`), serves `/lit/loop-kit.js` instead of the
|
|
79
|
+
stage kit, adds JetBrains Mono (bundled, `fonts/JetBrainsMono-wght-latin.woff2`) to `/lit/fonts.css`,
|
|
80
|
+
and injects an init script that makes every clock and entropy source throw. It runs three browsers
|
|
81
|
+
one after another: the master (frames, then title masks), a fresh replay, and a shuffled seek pass.
|
|
67
82
|
|
|
68
83
|
## Sound
|
|
69
84
|
|
|
@@ -5,6 +5,9 @@ animator and the editor: you write one page that draws the film as a function of
|
|
|
5
5
|
renderer captures it frame by frame, deterministically, then gates, checks and encodes it. The page
|
|
6
6
|
is yours; the kit gives motion primitives, never ready-made scenes, objects or layouts.
|
|
7
7
|
|
|
8
|
+
A README cover loop uses a different page contract: `cover/index.html` draws `renderFrame(t)` for a
|
|
9
|
+
loop time the renderer seeks in any order, with the loop kit instead of this one (`cover.md`).
|
|
10
|
+
|
|
8
11
|
## Files
|
|
9
12
|
|
|
10
13
|
- `<out>/treatment.json`: the treatment (see `treatment.md`).
|
|
@@ -6,6 +6,10 @@ with exit 16 (`BLOCKED_TREATMENT_INVALID`) naming the first field that fails. A
|
|
|
6
6
|
director's decision record: what the film is about, what the viewer sees in each beat, how it moves
|
|
7
7
|
and sounds, and what would make it excellent. Load the path reference only after `path` is set.
|
|
8
8
|
|
|
9
|
+
A README, hero, banner or cover loop is not a film and has no beats: its treatment sets
|
|
10
|
+
`"mode": "cover"` and follows `cover.md` instead of this file. A film render refuses a cover
|
|
11
|
+
treatment with exit 16, and the other way round.
|
|
12
|
+
|
|
9
13
|
## Fields
|
|
10
14
|
|
|
11
15
|
"Normalized" below means NFC, lowercase, then every space and punctuation mark removed. Before any
|
|
@@ -35,12 +35,14 @@ Usage:
|
|
|
35
35
|
node render.mjs perf --brief <brief.json> --out <dir>
|
|
36
36
|
node render.mjs gate --out <dir> [--viewed N]
|
|
37
37
|
node render.mjs stage --out <dir> [--round 1|2|3] [--stills-only] [--detach]
|
|
38
|
+
node render.mjs cover --out <dir> [--round 1|2|3] [--window t0,t1] [--detach]
|
|
38
39
|
node render.mjs sound --out <dir>
|
|
39
40
|
node render.mjs look --out <dir> --round 1|2|3 --answers <answers.json>
|
|
40
41
|
Options: --samples N (master default 4) --shutter S (default 0.5) --scale 1..4 --word-timing (Tier 3, opt-in)
|
|
41
42
|
Exit codes: 0 OK, 10 BLOCKED_NO_CHROME, 11 BLOCKED_NO_WEBGL2, 12 BLOCKED_NO_FFMPEG_FOR_VIDEO,
|
|
42
43
|
13 GATE_FAIL_QA, 14 BLOCKED_DEPS_NOT_PREWARMED, 15 BLOCKED_FONT_FETCH, 16 BLOCKED_TREATMENT_INVALID,
|
|
43
|
-
17 STAGE_CONTRACT_ERROR, 18 STAGE_NONDETERMINISTIC, 19 STAGE_NETWORK_REQUEST, 20 SOUND_INVALID
|
|
44
|
+
17 STAGE_CONTRACT_ERROR, 18 STAGE_NONDETERMINISTIC, 19 STAGE_NETWORK_REQUEST, 20 SOUND_INVALID,
|
|
45
|
+
21 BLOCKED_NO_COVER_ENCODER, 22 BLOCKED_ASSET_MISSING`;
|
|
44
46
|
|
|
45
47
|
const pageScripts = ["common.js", "gl.js", "lines.js", "passes.js", "post.js", "type.js", "scenes.js", "engine.js"];
|
|
46
48
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
@@ -107,7 +109,7 @@ export function planRun(brief, briefPath, args, state) {
|
|
|
107
109
|
else {
|
|
108
110
|
const target = path.join(args.runDir, "audio-grid.json");
|
|
109
111
|
const result = spawnSync(venv.python, [path.join(skillRoot, "audio-analysis.py"), audioFile, target], { encoding: "utf8", timeout: 300000 });
|
|
110
|
-
if (result.status !== 0) warnings.push(`audio analysis failed; using text reading time: ${(result.stderr || result.error?.message || "").trim().split(
|
|
112
|
+
if (result.status !== 0) warnings.push(`audio analysis failed; using text reading time: ${(result.stderr || result.error?.message || "").trim().split(/\r?\n/u).at(-1)}`);
|
|
111
113
|
else {
|
|
112
114
|
const grid = JSON.parse(readFileSync(target, "utf8"));
|
|
113
115
|
if (grid.beatGrid.length >= 2 && grid.bpm >= 40 && grid.bpm <= 240) { audioTier = "librosa-beat-grid"; beatGrid = grid.beatGrid; bpm = grid.bpm; audio = grid; }
|
|
@@ -443,8 +445,8 @@ function rerender(out, briefPath, frames, samples, shutter, rung, tag) {
|
|
|
443
445
|
const result = spawnSync(process.execPath, [fileURLToPath(import.meta.url), "frames", "--brief", briefPath, "--out", path.join(out, ".run", tag), "--plan", path.join(out, ".run", "plan.json"),
|
|
444
446
|
"--frames", frames.join(","), "--samples", String(samples), "--shutter", String(shutter), "--rung", rung], { encoding: "utf8", timeout: 900000, maxBuffer: 16_000_000 });
|
|
445
447
|
rmSync(path.join(out, ".run", tag), { recursive: true, force: true });
|
|
446
|
-
if (result.status !== 0) throw new Error(`determinism re-render (${rung}) failed: ${(result.stderr || result.error?.message || "").trim().split(
|
|
447
|
-
return JSON.parse(result.stdout.trim().split(
|
|
448
|
+
if (result.status !== 0) throw new Error(`determinism re-render (${rung}) failed: ${(result.stderr || result.error?.message || "").trim().split(/\r?\n/u).slice(-2).join(" ")}`);
|
|
449
|
+
return JSON.parse(result.stdout.trim().split(/\r?\n/u).at(-1)).hashes;
|
|
448
450
|
}
|
|
449
451
|
|
|
450
452
|
function determinism(out, briefPath, frames, engine, plan) {
|
|
@@ -480,9 +482,10 @@ function manifestFor(plan, engine, extra) {
|
|
|
480
482
|
function lookMode(argv, out) {
|
|
481
483
|
const round = Number(option(argv, "round", "0"));
|
|
482
484
|
const entry = recordLook(out, round, option(argv, "answers") ? path.resolve(option(argv, "answers")) : undefined);
|
|
483
|
-
console.log(`look round ${entry.round} recorded on the ${
|
|
485
|
+
console.log(`look round ${entry.round} recorded on the ${{ film: "final render", cover: "cover render" }[entry.kind] ?? "stills set"}: ${Object.keys(entry.frames).length} frame(s) viewed${entry.defects?.length ? `, ${entry.defects.length} defect(s) logged` : ""}${entry.blocked ? " (no vision tool: nobody viewed the frames)" : ""}`);
|
|
484
486
|
if (entry.needsAnotherRound.length) console.log(`another round is needed (questions ${entry.needsAnotherRound.join(", ")})${entry.round >= 3 ? "; round 3 is the last, so deliver with these open items stated" : ""}`);
|
|
485
487
|
else if (entry.kind === "stills") console.log("next: make the change you named, then render the film with the next --round");
|
|
488
|
+
else if (entry.kind === "cover" && entry.round === 1) console.log("next: make the change you named (check a logged defect with `cover --window t0,t1`), then render the cover with --round 2");
|
|
486
489
|
else console.log("next: node gate.mjs --done <dir>");
|
|
487
490
|
return EXIT.OK;
|
|
488
491
|
}
|
|
@@ -531,10 +534,30 @@ async function stageMode(argv, out, viewed) {
|
|
|
531
534
|
return gate.exitCode;
|
|
532
535
|
}
|
|
533
536
|
|
|
537
|
+
// `cover`: a README cover loop (engine/cover/). Every round is a full render gated on the decoded file;
|
|
538
|
+
// `--window t0,t1` re-renders only the frames around a logged defect. A detached run reports through .run/.
|
|
539
|
+
async function coverMode(argv, out) {
|
|
540
|
+
const round = Number(option(argv, "round", "1"));
|
|
541
|
+
if (!Number.isInteger(round) || round < 1 || round > 3) throw new Error("--round must be 1, 2 or 3");
|
|
542
|
+
if (argv.includes("--detach")) {
|
|
543
|
+
const { detachStage } = await import("./engine/stage/render.mjs");
|
|
544
|
+
const detached = detachStage(out, argv.slice(1), "cover");
|
|
545
|
+
console.log(`rendering the cover in the background (pid ${detached.pid}); the exit code lands in ${detached.exitFile}; log: ${detached.logFile}`);
|
|
546
|
+
return EXIT.OK;
|
|
547
|
+
}
|
|
548
|
+
const cover = await import("./engine/cover/render.mjs");
|
|
549
|
+
const span = option(argv, "window");
|
|
550
|
+
const result = span === undefined
|
|
551
|
+
? await cover.renderCover(out, { round, log })
|
|
552
|
+
: await cover.renderCoverWindow(out, { round, window: span.split(",").map(Number), log });
|
|
553
|
+
process.stdout.write(result.report);
|
|
554
|
+
return result.code;
|
|
555
|
+
}
|
|
556
|
+
|
|
534
557
|
async function run(argv) {
|
|
535
558
|
const mode = argv[0];
|
|
536
559
|
if (!mode || mode === "--help" || argv.includes("--help")) { console.log(usage); return EXIT.OK; }
|
|
537
|
-
if (!["film", "stills", "sheet", "video", "perf", "gate", "frames", "stage", "sound", "look"].includes(mode)) { console.error(usage); return 2; }
|
|
560
|
+
if (!["film", "stills", "sheet", "video", "perf", "gate", "frames", "stage", "cover", "sound", "look"].includes(mode)) { console.error(usage); return 2; }
|
|
538
561
|
const out = option(argv, "out") ? path.resolve(option(argv, "out")) : undefined;
|
|
539
562
|
if (!out) throw new Error("--out <dir> is required");
|
|
540
563
|
const viewed = argv.includes("--viewed") ? Number(option(argv, "viewed", "0")) || 0 : undefined;
|
|
@@ -544,6 +567,7 @@ async function run(argv) {
|
|
|
544
567
|
return result.exitCode;
|
|
545
568
|
}
|
|
546
569
|
if (mode === "stage") return stageMode(argv, out, viewed);
|
|
570
|
+
if (mode === "cover") return coverMode(argv, out);
|
|
547
571
|
if (mode === "sound") return soundMode(out);
|
|
548
572
|
if (mode === "look") return lookMode(argv, out);
|
|
549
573
|
let treatment = null;
|