@litfamily/litopencode 1.0.30 → 1.0.32
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 +6 -6
- package/README.md +6 -6
- package/dist/activation-probe.js +1 -0
- package/dist/activation-prompt-utils.d.ts +1 -1
- package/dist/activation-reentry.d.ts +20 -0
- package/dist/activation-reentry.js +110 -1
- package/dist/activation-routing.d.ts +2 -1
- package/dist/activation-routing.js +100 -11
- package/dist/activation-workflow-prompts.d.ts +4 -0
- package/dist/activation-workflow-prompts.js +30 -0
- package/dist/activation.d.ts +1 -1
- package/dist/activation.js +44 -5
- package/dist/adhd-next.d.ts +51 -0
- package/dist/adhd-next.js +150 -0
- package/dist/cli/managed-skill-assets.d.ts +1 -1
- package/dist/cli/managed-skill-assets.js +7 -1
- package/dist/cli/native-skill-integrity.js +1 -1
- package/dist/cli.js +7 -1
- package/dist/commands.d.ts +28 -1
- package/dist/commands.js +48 -1
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/ledger.js +12 -1
- package/dist/secret-shapes.js +5 -5
- package/dist/skills.d.ts +22 -1
- package/dist/skills.js +36 -0
- package/dist/start-work-typed.d.ts +34 -0
- package/dist/start-work-typed.js +156 -0
- package/dist/state.d.ts +2 -0
- package/dist/state.js +2 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +20 -1
- package/dist/tui/mission-band.js +73 -16
- package/dist/tui/mission-looks.d.ts +38 -0
- package/dist/tui/mission-looks.js +73 -0
- package/dist/tui/mission-model.d.ts +2 -0
- package/dist/tui/mission-model.js +3 -1
- package/dist/tui/mission-rainbow.d.ts +33 -0
- package/dist/tui/mission-rainbow.js +90 -0
- package/dist/tui/mission-source.d.ts +48 -0
- package/dist/tui/mission-source.js +184 -2
- package/dist/tui/mission-text.d.ts +2 -0
- package/dist/tui/mission-text.js +9 -1
- package/dist/tui/mission-threads.d.ts +50 -0
- package/dist/tui/mission-threads.js +119 -0
- package/dist/tui.js +84 -6
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/session-hooks.js +38 -5
- package/dist/workflow-stack.d.ts +53 -0
- package/dist/workflow-stack.js +196 -0
- package/docs/assets/readme/badge-version.svg +1 -1
- package/docs/assets/skills/lit-schematic.webp +0 -0
- package/docs/reference-Ko-KR.md +30 -2
- package/docs/reference.md +34 -2
- package/package.json +1 -1
- package/skills/frontend-ui-ux/SKILL.md +3 -2
- package/skills/frontend-ui-ux/kit/components.css +414 -0
- package/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/skills/frontend-ui-ux/references/brand-and-imagery.md +2 -2
- package/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/skills/frontend-ui-ux/references/craft-floor.md +6 -2
- package/skills/frontend-ui-ux/references/default-editorial-pixel.json +3 -0
- package/skills/frontend-ui-ux/references/house-styles.md +236 -0
- package/skills/frontend-ui-ux/references/motion-guide.md +2 -2
- package/skills/frontend-ui-ux/references/production.md +3 -1
- package/skills/frontend-ui-ux/references/slop-register.md +4 -4
- package/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/skills/frontend-ui-ux/scripts/probe-lock.mjs +235 -0
- package/skills/frontend-ui-ux/scripts/probe-page.mjs +34 -12
- package/skills/frontend-ui-ux/scripts/probe-static.mjs +16 -3
- package/skills/frontend-ui-ux/scripts/probe-thresholds.json +8 -2
- package/skills/frontend-ui-ux/scripts/probe.mjs +35 -7
- package/skills/lit-adhd/SKILL.md +208 -0
- package/skills/lit-adhd/references/brief-format.md +43 -0
- package/skills/lit-adhd/references/card-examples.md +82 -0
- package/skills/lit-adhd/references/status-taxonomy.md +29 -0
- package/skills/lit-adhd/scripts/rewind.mjs +835 -0
- package/skills/lit-diagram-drawer/SKILL.md +4 -3
- package/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/skills/lit-diagram-drawer/references/verifier-guide.md +1 -0
- package/skills/lit-diagram-drawer/scripts/craft-thresholds.json +1 -2
- package/skills/lit-diagram-drawer/scripts/visual-quality.mjs +1 -2
- package/skills/lit-docx/SKILL.md +2 -1
- package/skills/lit-handoff/SKILL.md +2 -0
- package/skills/lit-hijack/SKILL.md +422 -0
- package/skills/lit-hijack/references/capture-and-provenance.md +87 -0
- package/skills/lit-hijack/references/clean-room-build.md +111 -0
- package/skills/lit-hijack/references/distil-and-licence.md +100 -0
- package/skills/lit-hijack/references/gap-and-port-design.md +126 -0
- package/skills/lit-hijack/references/parity-and-report.md +110 -0
- package/skills/lit-hijack/scripts/hijack.mjs +993 -0
- package/skills/lit-plan/SKILL.md +2 -0
- package/skills/lit-pptx/SKILL.md +27 -11
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/skills/lit-pptx/fonts/jetbrains-mono/SOURCE.md +17 -0
- package/skills/lit-pptx/references/density-and-fill.md +9 -3
- package/skills/lit-pptx/references/direction-step.md +21 -6
- package/skills/lit-pptx/references/examples/04-lecture-chalk-ko.md +3 -6
- package/skills/lit-pptx/references/examples/05-research-talk-paper-en.md +1 -5
- package/skills/lit-pptx/references/examples/08-reliability-keynote-night-en.md +0 -4
- package/skills/lit-pptx/references/examples/09-status-update-ledger-en.md +2 -2
- package/skills/lit-pptx/references/examples/11-forecast-review-night-ko.md +4 -7
- package/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/skills/lit-pptx/references/layout-families.md +65 -47
- package/skills/lit-pptx/references/title-treatments.md +83 -134
- package/skills/lit-pptx/references/tonalities/atlas.md +3 -3
- package/skills/lit-pptx/references/tonalities/bauhaus.md +140 -0
- package/skills/lit-pptx/references/tonalities/chalk.md +7 -6
- package/skills/lit-pptx/references/tonalities/computational-brutalism.md +150 -0
- package/skills/lit-pptx/references/tonalities/gazette.md +1 -1
- package/skills/lit-pptx/references/tonalities/ledger.md +1 -1
- package/skills/lit-pptx/references/tonalities/neo-brutalism.md +161 -0
- package/skills/lit-pptx/references/tonalities/newsroom.md +144 -0
- package/skills/lit-pptx/references/tonalities/night.md +1 -1
- package/skills/lit-pptx/references/tonalities/paper.md +2 -2
- package/skills/lit-pptx/references/tonalities/riso.md +137 -0
- package/skills/lit-pptx/references/tonalities/signal.md +3 -3
- package/skills/lit-pptx/references/tonalities/studio.md +7 -6
- package/skills/lit-pptx/run.mjs +2 -1
- package/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/skills/lit-pptx/scripts/craft-thresholds.json +4 -8
- package/skills/lit-pptx/scripts/craft_extras.py +56 -3
- package/skills/lit-pptx/scripts/deck_output.py +75 -90
- package/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/skills/lit-pptx/scripts/layout_inventory.py +12 -5
- package/skills/lit-pptx/scripts/lib/grid-resolver.js +101 -110
- package/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/skills/lit-pptx/scripts/lib/render-pack.js +373 -566
- package/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/skills/lit-pptx/scripts/qa_deck.py +10 -11
- package/skills/lit-pptx/scripts/render_pages.py +118 -0
- package/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/skills/lit-recap/SKILL.md +4 -0
- package/skills/lit-schematic/SKILL.md +118 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/skills/lit-schematic/references/journal-figures.md +72 -0
- package/skills/lit-schematic/references/kit-api.md +134 -0
- package/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/skills/lit-schematic/references/verification.md +100 -0
- package/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/skills/lit-schematic/scripts/lib/fonts.mjs +25 -0
- package/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/skills/lit-schematic/scripts/render.mjs +233 -0
- package/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/skills/lit-scientific-visualization/SKILL.md +27 -4
- package/skills/lit-typographic-motion/SKILL.md +19 -17
- package/skills/lit-typographic-motion/THIRD_PARTY_NOTICES +9 -0
- package/skills/lit-typographic-motion/engine/accents.mjs +6 -0
- package/skills/lit-typographic-motion/engine/assets.mjs +282 -0
- package/skills/lit-typographic-motion/engine/blender/runner.py +228 -0
- package/skills/lit-typographic-motion/engine/blender.mjs +126 -0
- package/skills/lit-typographic-motion/engine/board.mjs +138 -0
- package/skills/lit-typographic-motion/engine/cover/assets.mjs +11 -8
- package/skills/lit-typographic-motion/engine/cover/capture.mjs +6 -6
- package/skills/lit-typographic-motion/engine/cover/render.mjs +20 -7
- package/skills/lit-typographic-motion/engine/cover/treatment.mjs +30 -3
- package/skills/lit-typographic-motion/engine/fill.mjs +54 -0
- package/skills/lit-typographic-motion/engine/gallery.mjs +118 -0
- package/skills/lit-typographic-motion/engine/house.mjs +42 -0
- package/skills/lit-typographic-motion/engine/look.mjs +65 -8
- package/skills/lit-typographic-motion/engine/loudness.mjs +101 -0
- package/skills/lit-typographic-motion/engine/scaffold.mjs +219 -0
- package/skills/lit-typographic-motion/engine/score.mjs +594 -0
- package/skills/lit-typographic-motion/engine/sound.mjs +9 -57
- package/skills/lit-typographic-motion/engine/stage/capture.mjs +23 -9
- package/skills/lit-typographic-motion/engine/stage/clock.js +8 -2
- package/skills/lit-typographic-motion/engine/stage/cover-init.js +12 -1
- package/skills/lit-typographic-motion/engine/stage/frame-worker.mjs +15 -7
- package/skills/lit-typographic-motion/engine/stage/gate.mjs +48 -4
- package/skills/lit-typographic-motion/engine/stage/qa-page.js +16 -5
- package/skills/lit-typographic-motion/engine/stage/qa.mjs +148 -7
- package/skills/lit-typographic-motion/engine/stage/render.mjs +69 -7
- package/skills/lit-typographic-motion/engine/stage/scan.mjs +20 -1
- package/skills/lit-typographic-motion/engine/stage/stage-kit.js +85 -4
- package/skills/lit-typographic-motion/engine/stills.mjs +1 -1
- package/skills/lit-typographic-motion/engine/styles.mjs +221 -0
- package/skills/lit-typographic-motion/engine/text.mjs +1 -1
- package/skills/lit-typographic-motion/engine/treatment.mjs +190 -6
- package/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/skills/lit-typographic-motion/gate.mjs +18 -2
- package/skills/lit-typographic-motion/probe.mjs +13 -1
- package/skills/lit-typographic-motion/recipes/hero-3d/expected.json +64 -0
- package/skills/lit-typographic-motion/recipes/hero-3d/index.html +132 -0
- package/skills/lit-typographic-motion/recipes/orbit-loop/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/painted-light/expected.json +2 -0
- package/skills/lit-typographic-motion/recipes/weight-tide/expected.json +2 -0
- package/skills/lit-typographic-motion/references/cover.md +56 -13
- package/skills/lit-typographic-motion/references/craft-loop.md +20 -7
- package/skills/lit-typographic-motion/references/depth.md +112 -0
- package/skills/lit-typographic-motion/references/runtime.md +21 -6
- package/skills/lit-typographic-motion/references/sound.md +76 -0
- package/skills/lit-typographic-motion/references/stage.md +50 -7
- package/skills/lit-typographic-motion/references/studio.md +162 -0
- package/skills/lit-typographic-motion/references/style-bibles.md +18 -0
- package/skills/lit-typographic-motion/references/treatment.md +108 -9
- package/skills/lit-typographic-motion/render.mjs +60 -3
- package/skills/lit-typographic-motion/requirements-audio.txt +3 -3
- package/skills/lit-typographic-motion/styles/paper.json +185 -0
- package/skills/lit-typographic-motion/styles/swiss-signal.json +188 -0
- package/skills/lit-typographic-motion/styles/terminalcore.json +182 -0
- package/skills/lit-typographic-motion/styles/tidal.json +187 -0
- package/skills/litresearch/SKILL.md +2 -0
- package/skills/managed-skill-manifest.json +520 -91
- package/skills/readme-studio/SKILL.md +1 -1
- package/skills/readme-studio/templates/hyperframes/package-lock.json +3 -3
- package/skills/readme-studio/templates/remotion/package-lock.json +3 -3
- package/skills/review-work/SKILL.md +2 -0
- package/skills/start-work/SKILL.md +8 -0
- package/skills/workflow-loop/SKILL.md +4 -0
- package/tools/check-pack-payload.mjs +4 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
|
@@ -6,10 +6,10 @@
|
|
|
6
6
|
// aspect and loop, and the title box the contrast and size gates measure. Its text is inert data.
|
|
7
7
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
8
8
|
import path from "node:path";
|
|
9
|
-
import { COVER } from "../constants.mjs";
|
|
10
|
-
import { normalize, restates, TREATMENT_EXIT } from "../treatment.mjs";
|
|
9
|
+
import { COVER, EXIT } from "../constants.mjs";
|
|
10
|
+
import { normUrl, normalize, requestUrls, restates, TREATMENT_EXIT } from "../treatment.mjs";
|
|
11
11
|
|
|
12
|
-
export const RECIPES = Object.freeze(["painted-light", "weight-tide", "orbit-loop", "own"]);
|
|
12
|
+
export const RECIPES = Object.freeze(["hero-3d", "painted-light", "weight-tide", "orbit-loop", "own"]);
|
|
13
13
|
export const LAYERS = Object.freeze(["slow", "mid", "beat"]);
|
|
14
14
|
|
|
15
15
|
class Invalid extends Error {
|
|
@@ -28,6 +28,20 @@ function text(t, key, { maxSentences } = {}) {
|
|
|
28
28
|
return value;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// A link in the request names the product: the cover is built from its real logo, images and words,
|
|
32
|
+
// fetched by `render.mjs assets --cover` before the treatment, never from the name alone.
|
|
33
|
+
function checkProduct(t) {
|
|
34
|
+
const urls = requestUrls(t.request);
|
|
35
|
+
if (!("product" in t)) {
|
|
36
|
+
need(!urls.length, "product", `missing: the request links ${urls[0]}; fetch its real assets first with render.mjs assets --out <dir> --url <link> --cover, look at them, and set product { name, url } (references/cover.md, Named products)`);
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
need(t.product && typeof t.product === "object" && !Array.isArray(t.product), "product", "must be an object { name, url }");
|
|
40
|
+
text(t, "product.name");
|
|
41
|
+
if (urls.length) need(urls.includes(normUrl(t.product.url)), "product.url", `must be the link the request gives (${urls.join(", ")})`);
|
|
42
|
+
if ("unreachable" in t.product) text(t, "product.unreachable");
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
/** Validate a cover treatment. Returns { ok: true, cover } with the derived sizes, or { ok: false, field, detail }. */
|
|
32
46
|
export function validateCoverTreatment(t) {
|
|
33
47
|
try {
|
|
@@ -38,7 +52,10 @@ export function validateCoverTreatment(t) {
|
|
|
38
52
|
need(!restates(t.idea, t.request), "idea", "the idea restates the request; say what the loop does");
|
|
39
53
|
need(RECIPES.includes(t.recipe), "recipe", `must be one of ${RECIPES.join(", ")} (references/cover.md)`);
|
|
40
54
|
text(t, "recipeReason", { maxSentences: 2 });
|
|
55
|
+
text(t, "hero", { maxSentences: 2 });
|
|
56
|
+
text(t, "beat", { maxSentences: 2 });
|
|
41
57
|
text(t, "brandWord");
|
|
58
|
+
checkProduct(t);
|
|
42
59
|
need(words(t.brandWord) <= 3, "brandWord", "is the name, set once: at most three words");
|
|
43
60
|
if ("motto" in t) {
|
|
44
61
|
need(t.motto && typeof t.motto === "object", "motto", "must be { text, source: user | project, from? }");
|
|
@@ -103,6 +120,16 @@ export function loadCoverTreatment(out) {
|
|
|
103
120
|
if (parsed?.mode !== "cover") throw fail("mode", "this treatment is a film; render it with `render.mjs stage` or `render.mjs film` (references/treatment.md). A cover sets mode: cover");
|
|
104
121
|
const result = validateCoverTreatment(parsed);
|
|
105
122
|
if (!result.ok) throw fail(result.field, result.detail);
|
|
123
|
+
const product = result.cover.product;
|
|
124
|
+
if (product?.unreachable) throw Object.assign(new Error(`BLOCKED_ASSET_MISSING (${EXIT.BLOCKED_ASSET_MISSING}): ${product.name}'s real assets could not be reached (${product.unreachable}); ask the user for its logo or README images, put them in ${path.join(out, "cover", "assets")} and list them in assets.json with source user`), { exitCode: EXIT.BLOCKED_ASSET_MISSING });
|
|
125
|
+
if (product) {
|
|
126
|
+
const file = path.join(out, "product.json");
|
|
127
|
+
if (!existsSync(file)) throw fail("product", `no ${file}: run render.mjs assets --out ${out} --url ${product.url} --cover first`);
|
|
128
|
+
let fetched = [];
|
|
129
|
+
try { fetched = JSON.parse(readFileSync(file, "utf8"))?.product?.assets ?? []; } catch (error) { throw fail("product", `${file} is not valid JSON (${error.message}); run render.mjs assets --out ${out} --url ${product.url} --cover again`); }
|
|
130
|
+
const astray = fetched.filter((asset) => !String(asset?.path ?? "").startsWith("cover/assets/"));
|
|
131
|
+
if (astray.length) throw fail("product", `${astray[0].path} sits outside cover/assets/, where the cover page cannot load it; run render.mjs assets --out ${out} --url ${product.url} --cover again`);
|
|
132
|
+
}
|
|
106
133
|
const first = path.join(out, ".run", "treatment-first.json");
|
|
107
134
|
mkdirSync(path.dirname(first), { recursive: true });
|
|
108
135
|
if (!existsSync(first)) writeFileSync(first, JSON.stringify(parsed, null, 2) + "\n");
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// How full a rendered frame is. `busyShare` reads the share of 48 px cells that carry anything
|
|
2
|
+
// (DEAD-AIR) and `regionFill` reads it per region of a 4 x 3 grid (RICH-FILL). A dense reference
|
|
3
|
+
// frame (a full dashboard) measured 0.57 or more in every region; films with an empty half measured
|
|
4
|
+
// 0.00-0.10 there. Opaque RGBA frames in, 0..1 shares out; pure, so the gate tests drive it directly.
|
|
5
|
+
export const FILL_GRID = Object.freeze({ cols: 4, rows: 3 });
|
|
6
|
+
|
|
7
|
+
const luma8 = (p, o) => Math.round(0.299 * p[o] + 0.587 * p[o + 1] + 0.114 * p[o + 2]);
|
|
8
|
+
|
|
9
|
+
// A cell is busy when its luma varies (sd > 6) or its mean sits more than 16 levels off the frame's
|
|
10
|
+
// ground, the most common luma smoothed over +-3 levels. Sampled on a 3 px lattice.
|
|
11
|
+
export function busyShare(rgba, width, height, { cell = 48, step = 3 } = {}) {
|
|
12
|
+
const hist = new Uint32Array(256);
|
|
13
|
+
for (let y = 0; y < height; y += step) for (let x = 0; x < width; x += step) hist[luma8(rgba, (y * width + x) * 4)] += 1;
|
|
14
|
+
let ground = 0;
|
|
15
|
+
let best = -1;
|
|
16
|
+
for (let i = 0; i < 256; i++) {
|
|
17
|
+
let s = 0;
|
|
18
|
+
for (let k = Math.max(0, i - 3); k <= Math.min(255, i + 3); k++) s += hist[k];
|
|
19
|
+
if (s > best) [best, ground] = [s, i];
|
|
20
|
+
}
|
|
21
|
+
let busy = 0;
|
|
22
|
+
let cells = 0;
|
|
23
|
+
for (let cy = 0; cy + cell <= height; cy += cell) {
|
|
24
|
+
for (let cx = 0; cx + cell <= width; cx += cell) {
|
|
25
|
+
let sum = 0, sumSq = 0, n = 0;
|
|
26
|
+
for (let y = cy; y < cy + cell; y += step) for (let x = cx; x < cx + cell; x += step) {
|
|
27
|
+
const l = luma8(rgba, (y * width + x) * 4);
|
|
28
|
+
sum += l; sumSq += l * l; n += 1;
|
|
29
|
+
}
|
|
30
|
+
const mean = sum / n;
|
|
31
|
+
if (Math.sqrt(Math.max(0, sumSq / n - mean * mean)) > 6 || Math.abs(mean - ground) > 16) busy += 1;
|
|
32
|
+
cells += 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return cells ? busy / cells : 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The busyShare of each region of the FILL_GRID, row by row.
|
|
39
|
+
export function regionFill(rgba, width, height) {
|
|
40
|
+
const out = [];
|
|
41
|
+
const rw = Math.floor(width / FILL_GRID.cols);
|
|
42
|
+
const rh = Math.floor(height / FILL_GRID.rows);
|
|
43
|
+
for (let r = 0; r < FILL_GRID.rows; r++) {
|
|
44
|
+
for (let c = 0; c < FILL_GRID.cols; c++) {
|
|
45
|
+
const sub = new Uint8Array(rw * rh * 4);
|
|
46
|
+
for (let y = 0; y < rh; y++) {
|
|
47
|
+
const from = ((r * rh + y) * width + c * rw) * 4;
|
|
48
|
+
sub.set(rgba.subarray(from, from + rw * 4), y * rw * 4);
|
|
49
|
+
}
|
|
50
|
+
out.push(busyShare(sub, rw, rh));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// `render.mjs styles --out <dir> [--format 16:9|9:16]`: the style gallery. One short demo film, the
|
|
2
|
+
// same treatment and the same drawings, rendered as a board round in every house style, so a person
|
|
3
|
+
// can see what each style looks like before asking for a film. Each style's board lands in
|
|
4
|
+
// `<dir>/<style>/stills/board.png`, and `<dir>/styles.png` puts them side by side. The demo also
|
|
5
|
+
// proves the styles and the scaffold pass the board check as shipped.
|
|
6
|
+
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { EXIT } from "./constants.mjs";
|
|
9
|
+
import { areaResize, decodePng, encodePng } from "./png.mjs";
|
|
10
|
+
import { scaffoldHtml } from "./scaffold.mjs";
|
|
11
|
+
import { tile } from "./stills.mjs";
|
|
12
|
+
import { STYLE_IDS, readStyle } from "./styles.mjs";
|
|
13
|
+
|
|
14
|
+
const COPY = ["작게 시작해서 끝까지", "One clock for every frame", "한 화면에 한 가지 생각", "Same house, every film"];
|
|
15
|
+
|
|
16
|
+
// The demo treatment for one style: four shots, one of each text layout, 12 s.
|
|
17
|
+
export function demoTreatment(styleId, format = "16:9") {
|
|
18
|
+
const style = readStyle(styleId);
|
|
19
|
+
const hand = (i) => style.transitions[i % style.transitions.length];
|
|
20
|
+
const vertical = format === "9:16" ? " vertical" : "";
|
|
21
|
+
const beats = [
|
|
22
|
+
{ t0: 0, t1: 3, layout: "title", hero: "The opening line lands while a ring draws itself behind it.", purpose: "Open on the promise.", onScreen: "A ring drawing on behind the opening line.", motion: "The ring draws on; the line rises in.", sound: "Pad opens with a hit on the first word." },
|
|
23
|
+
{ t0: 3, t1: 6, layout: "split", hero: "Five bars rise in turn beside the line.", purpose: "Show the system.", onScreen: "Five bars rising beside one line.", motion: "Bars rise in stagger.", sound: "Hit on the cut." },
|
|
24
|
+
{ t0: 6, t1: 9, layout: "split-alt", hero: "A dot travels a path from left to right.", purpose: "Show one idea per screen.", onScreen: "A dot crossing a drawn path.", motion: "The dot travels the path.", sound: "A rise into the close." },
|
|
25
|
+
{ t0: 9, t1: 12, layout: "stage", hero: "A grid of squares settles above the closing line.", purpose: "Close on the house.", onScreen: "A settling grid of squares over a closing caption.", motion: "Squares settle in stagger.", sound: "Closing cadence." }
|
|
26
|
+
].map((b, i, all) => ({ ...b, lines: [i], transition: i === all.length - 1 ? "end" : hand(i) }));
|
|
27
|
+
const colours = [["ground", "ground"], ["ink", "copy"], ["signal", "signal"], ["muted", "grid"]];
|
|
28
|
+
return {
|
|
29
|
+
// A calibration sheet, not a film: the same simple demo in every style, so the styles compare.
|
|
30
|
+
request: `render a 12 s${vertical} minimal demo film in the ${styleId} style`,
|
|
31
|
+
register: "minimal",
|
|
32
|
+
genre: "other", path: "stage", pathReason: "The demo draws shapes beside its type, which the type engine cannot draw.",
|
|
33
|
+
style: styleId,
|
|
34
|
+
idea: "Four screens, one per layout, so the house style can be judged before a film is made.",
|
|
35
|
+
audience: "Someone choosing a house style for their films.", channel: "A gallery sheet viewed before a film is made, played with sound.",
|
|
36
|
+
format, formatReason: format === "9:16" ? "The gallery shows the vertical layouts." : "The gallery shows the landscape layouts.",
|
|
37
|
+
durationSec: 12, beats,
|
|
38
|
+
story: { expect: "A style sheet is four unrelated pictures.", turn: "The dot crosses its path and shows the four screens are one system.", beat: 2, payoff: "The grid settles and the house reads as one piece." },
|
|
39
|
+
protagonist: { name: "the dot", kind: "object", look: "a small round dot in the signal colour, a twentieth of the frame high", acts: [{ beat: 0, action: "waits inside the ring" }, { beat: 1, action: "stands beside the bars" }, { beat: 2, action: "travels its path" }, { beat: 3, action: "settles into the grid" }] },
|
|
40
|
+
subject: { name: "the style demo", source: "invented", specifics: ["Four screens with a ring, bars, a travelling dot and a grid.", "Each screen uses one layout of the house grid."] },
|
|
41
|
+
visualDevices: [{ kind: "shape", role: "subject", beats: [0, 1, 2, 3] }, { kind: "path", role: "support", beats: [2] }],
|
|
42
|
+
typePlan: { faces: [style.faces.headline.family], hierarchy: "One line per screen in the style's headline or display size.", maxWordsOnScreen: 6 },
|
|
43
|
+
palette: colours.map(([key, role]) => ({ hex: style.palette[key], role })),
|
|
44
|
+
sound: { mode: "generated", plan: "The style's own bed with a hit on the cuts and a closing cadence.", palette: style.beds[0].palette, key: style.beds[0].key, tempo: style.beds[0].tempo },
|
|
45
|
+
copy: { source: "invented", lines: COPY },
|
|
46
|
+
inventions: ["the style demo (its four screens)", "all four copy lines"],
|
|
47
|
+
ambition: "Each screen shows one layout cleanly so the style's type, colour and motion read at a glance."
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// The demo's drawings: generic shapes in the style's own colours, one per shot.
|
|
52
|
+
const DEMO_DRAW = [
|
|
53
|
+
`(s, svg) => {
|
|
54
|
+
const z = LitStage.zone(s.layout, "subject");
|
|
55
|
+
const r = Math.min(z.w, z.h) * 0.34;
|
|
56
|
+
const len = 2 * Math.PI * r;
|
|
57
|
+
svg.innerHTML = \`<circle cx="\${z.w * 0.5}" cy="\${z.h * 0.5}" r="\${r}" fill="none" stroke="var(--lit-line)" stroke-width="6"/><circle cx="\${z.w * 0.5}" cy="\${z.h * 0.5}" r="\${r}" fill="none" stroke="var(--lit-signal)" stroke-width="6" stroke-dasharray="\${len}" stroke-dashoffset="\${len * (1 - s.enter(0, 1.6))}"/>\`;
|
|
58
|
+
},`,
|
|
59
|
+
`(s, svg) => {
|
|
60
|
+
const z = LitStage.zone(s.layout, "subject");
|
|
61
|
+
const w = z.w / 9;
|
|
62
|
+
svg.innerHTML = [0, 1, 2, 3, 4].map((k) => { const h = z.h * (0.3 + 0.12 * k) * s.enter(0.1 + k * T.stagger.item * 2, T.dur.hero); return \`<rect x="\${w * (1 + k * 1.6)}" y="\${z.h * 0.9 - h}" width="\${w}" height="\${h}" fill="\${k === 4 ? "var(--lit-accent, var(--lit-signal))" : "var(--lit-signal)"}"/>\`; }).join("");
|
|
63
|
+
},`,
|
|
64
|
+
`(s, svg) => {
|
|
65
|
+
const z = LitStage.zone(s.layout, "subject");
|
|
66
|
+
const x = z.w * (0.1 + 0.8 * T.ease.move(LitStage.clamp(s.local / (s.length * 0.8))));
|
|
67
|
+
svg.innerHTML = \`<path d="M\${z.w * 0.1} \${z.h * 0.6} C \${z.w * 0.35} \${z.h * 0.2}, \${z.w * 0.65} \${z.h * 0.9}, \${z.w * 0.9} \${z.h * 0.45}" fill="none" stroke="var(--lit-line)" stroke-width="5"/><circle cx="\${x}" cy="\${z.h * 0.55}" r="\${z.h * 0.06}" fill="var(--lit-signal)"/>\`;
|
|
68
|
+
},`,
|
|
69
|
+
`(s, svg) => {
|
|
70
|
+
const z = LitStage.zone(s.layout, "subject");
|
|
71
|
+
const n = 8;
|
|
72
|
+
const c = Math.min(z.w / (n + 2), z.h / 4);
|
|
73
|
+
const cells = [];
|
|
74
|
+
for (let k = 0; k < n * 2; k++) { const e = s.enter(0.05 * k, T.dur.move); cells.push(\`<rect x="\${c * (1 + (k % n)) + c * 0.1}" y="\${z.h * 0.3 + c * Math.floor(k / n) + (1 - e) * 30}" width="\${c * 0.8}" height="\${c * 0.8}" opacity="\${e}" fill="\${k === 11 ? "var(--lit-accent, var(--lit-signal))" : "var(--lit-muted)"}"/>\`); }
|
|
75
|
+
svg.innerHTML = cells.join("");
|
|
76
|
+
},`
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
// The demo page: the scaffold with the demo drawings in its DRAW slots.
|
|
80
|
+
export function demoPage(treatment) {
|
|
81
|
+
let k = 0;
|
|
82
|
+
return scaffoldHtml(treatment).replace(/ {4}\(s, svg, t\) => \{\},/gu, () => ` ${DEMO_DRAW[k++ % DEMO_DRAW.length]}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const toRgba = (image) => {
|
|
86
|
+
if (image.channels === 4) return image.pixels;
|
|
87
|
+
const out = Buffer.alloc(image.width * image.height * 4);
|
|
88
|
+
for (let i = 0, j = 0; i < image.pixels.length; i += image.channels, j += 4) { out[j] = image.pixels[i]; out[j + 1] = image.pixels[i + 1]; out[j + 2] = image.pixels[i + 2]; out[j + 3] = 255; }
|
|
89
|
+
return out;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// Renders the gallery: a board round per style, then one sheet of the four boards. `render` is
|
|
93
|
+
// renderStage; it is passed in so the gallery stays testable without a browser.
|
|
94
|
+
export async function stylesCommand(out, { format = "16:9", render, log = () => {} }) {
|
|
95
|
+
if (!["16:9", "9:16"].includes(format)) throw Object.assign(new Error("--format is 16:9 or 9:16"), { exitCode: 2 });
|
|
96
|
+
mkdirSync(out, { recursive: true });
|
|
97
|
+
const boards = [];
|
|
98
|
+
const verdicts = [];
|
|
99
|
+
for (const id of STYLE_IDS) {
|
|
100
|
+
const dir = path.join(out, id);
|
|
101
|
+
rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
mkdirSync(path.join(dir, "stage"), { recursive: true });
|
|
103
|
+
const treatment = demoTreatment(id, format);
|
|
104
|
+
writeFileSync(path.join(dir, "treatment.json"), `${JSON.stringify(treatment, null, 2)}\n`);
|
|
105
|
+
writeFileSync(path.join(dir, "stage", "index.html"), demoPage(treatment));
|
|
106
|
+
log(`styles: rendering the ${id} board`);
|
|
107
|
+
const result = await render(dir, { round: 1, stillsOnly: true });
|
|
108
|
+
const failed = result.board.failed.map((rule) => rule.id);
|
|
109
|
+
verdicts.push({ id, pass: failed.length === 0, text: `${id}: ${failed.length ? `board check FAIL (${failed.join(", ")})` : "board check PASS"}` });
|
|
110
|
+
boards.push(decodePng(readFileSync(path.join(dir, "stills", "board.png"))));
|
|
111
|
+
}
|
|
112
|
+
const images = boards.map((board) => areaResize(toRgba(board), board.width, board.height, 960, 540, 4));
|
|
113
|
+
const sheet = tile(images, 2, 960, 540, 8);
|
|
114
|
+
const file = path.join(out, "styles.png");
|
|
115
|
+
writeFileSync(file, encodePng(sheet.width, sheet.height, sheet.pixels, 4, 6));
|
|
116
|
+
const failing = verdicts.filter((verdict) => !verdict.pass);
|
|
117
|
+
return { code: failing.length ? EXIT.GATE_FAIL_QA : EXIT.OK, file, message: `STYLES: ${file}\n${verdicts.map((verdict) => verdict.text).join("\n")}` };
|
|
118
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The house file: `motion-house.json` beside a film's output folder (the user's working directory).
|
|
2
|
+
// A full render that passes its gate writes it, or appends the film to it; while it exists, the
|
|
3
|
+
// treatment validator holds every new film in that folder to its style unless the request names
|
|
4
|
+
// another. It is data the validator reads, so the second film starts from the first one's style
|
|
5
|
+
// without anyone having to remember it.
|
|
6
|
+
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { STYLE_IDS } from "./styles.mjs";
|
|
9
|
+
|
|
10
|
+
export const HOUSE_FILE = "motion-house.json";
|
|
11
|
+
export const HOUSE_SCHEMA = "litopencode.motion-house/v1";
|
|
12
|
+
const MAX_FILMS = 50;
|
|
13
|
+
|
|
14
|
+
export const houseFileFor = (out) => path.join(path.dirname(path.resolve(out)), HOUSE_FILE);
|
|
15
|
+
|
|
16
|
+
// The house record for an output folder, or null when there is none or it is unusable.
|
|
17
|
+
export function readHouse(out) {
|
|
18
|
+
const file = houseFileFor(out);
|
|
19
|
+
if (!existsSync(file)) return null;
|
|
20
|
+
try {
|
|
21
|
+
const house = JSON.parse(readFileSync(file, "utf8"));
|
|
22
|
+
if (house?.schema !== HOUSE_SCHEMA || !STYLE_IDS.includes(house.style)) return null;
|
|
23
|
+
return house;
|
|
24
|
+
} catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Record a passed film. The first film sets the house style; later films are appended. A film in a
|
|
30
|
+
// style the request named outright is appended without moving the house style.
|
|
31
|
+
export function recordHouse(out, { style, path: renderPath, format, durationSec, now = new Date() }) {
|
|
32
|
+
const file = houseFileFor(out);
|
|
33
|
+
const house = readHouse(out) ?? { schema: HOUSE_SCHEMA, style, createdAt: now.toISOString(), films: [] };
|
|
34
|
+
const dir = path.basename(path.resolve(out));
|
|
35
|
+
const films = (Array.isArray(house.films) ? house.films : []).filter((f) => f?.dir !== dir);
|
|
36
|
+
films.push({ dir, style, path: renderPath, format, durationSec, at: now.toISOString() });
|
|
37
|
+
const next = { ...house, films: films.slice(-MAX_FILMS) };
|
|
38
|
+
const tmp = `${file}.tmp`;
|
|
39
|
+
writeFileSync(tmp, `${JSON.stringify(next, null, 2)}\n`);
|
|
40
|
+
renameSync(tmp, file);
|
|
41
|
+
return next;
|
|
42
|
+
}
|
|
@@ -22,8 +22,14 @@ export const LOOK_QUESTIONS = Object.freeze([
|
|
|
22
22
|
"Is any element on screen without a job in its beat?",
|
|
23
23
|
"Could every copy line be pasted unchanged into a film about a different subject?"
|
|
24
24
|
]);
|
|
25
|
+
// A stage film is also asked whether it is as full as its register asks and whether its turn lands.
|
|
26
|
+
export const STAGE_LOOK_QUESTIONS = Object.freeze([
|
|
27
|
+
...LOOK_QUESTIONS,
|
|
28
|
+
"Is the film rich enough for its register (rich: layered, every region of the frame working; minimal: a few elements, none idle)?",
|
|
29
|
+
"Does the turn land: at the turn shot, does what the film set up visibly break and change, so a stranger would notice it?"
|
|
30
|
+
]);
|
|
25
31
|
// A "no" here means another round; for the others a "yes" does (Q7: a nameable gap).
|
|
26
|
-
const noMeansRevise = new Set([1, 2, 3, 5, 6]);
|
|
32
|
+
const noMeansRevise = new Set([1, 2, 3, 5, 6, 10, 11]);
|
|
27
33
|
// A cover is looked at for motion, the seam and damage, never for beats (references/cover.md).
|
|
28
34
|
export const COVER_LOOK_QUESTIONS = Object.freeze([
|
|
29
35
|
"Is something moving in every half-second window (the sheet's plot never lies flat on the red dashes)?",
|
|
@@ -33,9 +39,10 @@ export const COVER_LOOK_QUESTIONS = Object.freeze([
|
|
|
33
39
|
"Are all three speed layers visible: a slow perpetual layer, a mid swell, beat events?",
|
|
34
40
|
"Does anything read as a slide, a card swap, a title fade or an end card, or does light wash in place instead of travelling as an edge?",
|
|
35
41
|
"Is any glyph, glint or glow damaged: a colour block, a dark disc, a visible box?",
|
|
36
|
-
"Would a skilled motion designer add or remove something? Name it."
|
|
42
|
+
"Would a skilled motion designer add or remove something? Name it.",
|
|
43
|
+
"Does the hero act its beat once per loop: one clear action, at the beat's time, not a wobble?"
|
|
37
44
|
]);
|
|
38
|
-
const coverNoMeansRevise = new Set([1, 2, 3, 4, 5]);
|
|
45
|
+
const coverNoMeansRevise = new Set([1, 2, 3, 4, 5, 9]);
|
|
39
46
|
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
40
47
|
const shaFile = (file) => sha256(readFileSync(file));
|
|
41
48
|
const readJson = (file, fallback = null) => (existsSync(file) ? JSON.parse(readFileSync(file, "utf8")) : fallback);
|
|
@@ -107,7 +114,8 @@ export function recordLook(out, round, answersFile) {
|
|
|
107
114
|
const set = currentSet(out);
|
|
108
115
|
if (set.stills.round !== round) throw new LookError(`the latest stills set was rendered with --round ${set.stills.round}; look at the set of round ${round}`);
|
|
109
116
|
const cover = set.kind === "cover";
|
|
110
|
-
const
|
|
117
|
+
const stageFilm = !cover && readJson(path.join(out, "treatment.json"))?.path === "stage";
|
|
118
|
+
const questions = cover ? COVER_LOOK_QUESTIONS : stageFilm ? STAGE_LOOK_QUESTIONS : LOOK_QUESTIONS;
|
|
111
119
|
const blocked = input.blocked === "no-vision-tool";
|
|
112
120
|
const viewed = [...new Set([...(Array.isArray(input.viewed) ? input.viewed : []), ...(Array.isArray(input.answers) ? input.answers.map((answer) => answer?.frame).filter((frame) => frame && frame !== "sound-cues.json") : [])])];
|
|
113
121
|
const unknown = viewed.filter((frame) => typeof frame !== "string" || !set.files.includes(frame.replace(/^\.\//u, "")));
|
|
@@ -146,9 +154,10 @@ export function recordLook(out, round, answersFile) {
|
|
|
146
154
|
return entry;
|
|
147
155
|
}
|
|
148
156
|
|
|
149
|
-
// Every image the last round must have viewed: poster, contact sheet, every beat midpoint
|
|
157
|
+
// Every image the last round must have viewed: poster, contact sheet, storyboard, every beat midpoint
|
|
158
|
+
// and strip.
|
|
150
159
|
export function requiredFrames(set) {
|
|
151
|
-
return set.files.filter((file) => /(?:^|\/)poster\.png$|sheet\/contact\.png$|stills\/beat-\d+-mid\.png$|stills\/cut-\d+-strip\.png$/u.test(file));
|
|
160
|
+
return set.files.filter((file) => /(?:^|\/)poster\.png$|sheet\/contact\.png$|stills\/board\.png$|stills\/beat-\d+-mid\.png$|stills\/cut-\d+-strip\.png$/u.test(file));
|
|
152
161
|
}
|
|
153
162
|
|
|
154
163
|
function imageReads(out) {
|
|
@@ -207,14 +216,62 @@ export function coverDoneState(out) {
|
|
|
207
216
|
return result("DONE", 0);
|
|
208
217
|
}
|
|
209
218
|
|
|
219
|
+
// A film whose treatment asks for a preview (`preview: true`) is done at its board: a stills-only
|
|
220
|
+
// render whose layout check passed, looked at, storyboard included. The full render waits for the user.
|
|
221
|
+
function previewDone(out, { reasons, labels, result }) {
|
|
222
|
+
const stills = readJson(path.join(out, "stills", "stills.json"));
|
|
223
|
+
if (!stills) { reasons.push("no board yet: render with `stage --stills-only`"); return result("NOT DONE", 1); }
|
|
224
|
+
if (!stills.files.some((entry) => entry.kind === "board")) reasons.push("the stills set has no storyboard (stills/board.png); render with `stage --stills-only` again");
|
|
225
|
+
const check = readJson(path.join(out, "stills", "check.json"));
|
|
226
|
+
if (check) {
|
|
227
|
+
const failed = check.rules.filter((rule) => rule.status === "FAIL").map((rule) => rule.id);
|
|
228
|
+
if (failed.length) reasons.push(`the board check failed (${failed.join(", ")}); fix the page and render the board again`);
|
|
229
|
+
} else reasons.push("the board check result (stills/check.json) is missing");
|
|
230
|
+
const rounds = readJson(path.join(out, "look.json"), { rounds: [] }).rounds;
|
|
231
|
+
const last = rounds.at(-1);
|
|
232
|
+
if (!last) reasons.push("no look round on the board: open the storyboard and the stills with the read tool and record round 1");
|
|
233
|
+
else {
|
|
234
|
+
if (last.manifestSha256 !== shaFile(path.join(out, "stills", "stills.json"))) reasons.push("the look round was recorded before the latest board; look again and record it");
|
|
235
|
+
const required = stills.files.filter((entry) => ["board", "sheet", "beat"].includes(entry.kind)).map((entry) => entry.file);
|
|
236
|
+
const skipped = required.filter((file) => !Object.hasOwn(last.frames ?? {}, file));
|
|
237
|
+
if (skipped.length && !last.blocked) reasons.push(`the round did not view: ${skipped.join(", ")}`);
|
|
238
|
+
if (last.host === "opencode" && !last.blocked) {
|
|
239
|
+
const reads = imageReads(out);
|
|
240
|
+
const notRead = Object.entries(last.frames ?? {}).filter(([file, sha]) => !reads.some((read) => read.file === file && read.sha256 === sha)).map(([file]) => file);
|
|
241
|
+
if (notRead.length) reasons.push(`not opened with the read tool: ${notRead.join(", ")}`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
if (reasons.length) return result("NOT DONE", 1);
|
|
245
|
+
labels.push(`this is the storyboard preview (${path.join(out, "stills", "board.png")}); the film is not rendered yet, and the user can say go to render it`);
|
|
246
|
+
return last.blocked === "no-vision-tool" ? result("DONE_UNVIEWED", 3) : result("DONE", 0);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// The run a done call means. `given` is the directory named by a path or `--out` (empty or missing: the
|
|
250
|
+
// working directory, which is what an unset or empty shell variable leaves). When that folder holds no
|
|
251
|
+
// treatment.json, the nearest ancestor within three levels that does is the run, so the check also works
|
|
252
|
+
// from the film's stage/ or stills/ folder.
|
|
253
|
+
export function findRunDir(given, cwd = process.cwd()) {
|
|
254
|
+
const start = path.resolve(cwd, typeof given === "string" && given !== "" ? given : ".");
|
|
255
|
+
let dir = start;
|
|
256
|
+
for (let level = 0; level <= 3; level++) {
|
|
257
|
+
if (existsSync(path.join(dir, "treatment.json"))) return dir;
|
|
258
|
+
const parent = path.dirname(dir);
|
|
259
|
+
if (parent === dir) break;
|
|
260
|
+
dir = parent;
|
|
261
|
+
}
|
|
262
|
+
return start;
|
|
263
|
+
}
|
|
264
|
+
|
|
210
265
|
// Status: DONE (0), NOT DONE (1), DONE_WITH_OPEN_ITEMS (2, round 3 spent), DONE_UNVIEWED (3).
|
|
211
266
|
export function doneState(dir) {
|
|
212
|
-
const out =
|
|
267
|
+
const out = findRunDir(dir);
|
|
213
268
|
if (isCoverRun(out)) return coverDoneState(out);
|
|
214
269
|
const reasons = [], open = [];
|
|
215
|
-
const
|
|
270
|
+
const labels = [];
|
|
271
|
+
const result = (status, code) => ({ status, code, done: code === 0, reasons, open, downgraded, labels });
|
|
216
272
|
let treatment = null, downgraded = [];
|
|
217
273
|
try { ({ treatment } = loadTreatment(out)); } catch (error) { reasons.push(error.message); return result("NOT DONE", 1); }
|
|
274
|
+
if (treatment.preview === true) return previewDone(out, { reasons, labels, result });
|
|
218
275
|
downgraded = downgrades(readJson(path.join(out, ".run", "treatment-first.json")), treatment);
|
|
219
276
|
const manifestFile = path.join(out, "manifest.json");
|
|
220
277
|
const manifest = readJson(manifestFile);
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// Loudness for every generated track: the ITU-R BS.1770-4 meter, the sample peak, a lookahead limiter and
|
|
2
|
+
// the master stage that brings a mix to -16 LUFS under a -2 dBFS ceiling. Pure functions over Float64
|
|
3
|
+
// channels at 48 kHz; the sound bed and the score both end here.
|
|
4
|
+
export const RATE = 48000;
|
|
5
|
+
export const TARGET_LUFS = -16;
|
|
6
|
+
export const PEAK_CEILING_DBFS = -2;
|
|
7
|
+
|
|
8
|
+
// ITU-R BS.1770-4: K-weighting at 48 kHz, 400 ms blocks with 75 % overlap, -70 LUFS absolute and
|
|
9
|
+
// -10 LU relative gates.
|
|
10
|
+
const shelf = { b: [1.53512485958697, -2.69169618940638, 1.19839281085285], a: [1, -1.69065929318241, 0.73248077421585] };
|
|
11
|
+
const highPass = { b: [1, -2, 1], a: [1, -1.99004745483398, 0.99007225036621] };
|
|
12
|
+
function biquad(input, { b, a }) {
|
|
13
|
+
const out = new Float64Array(input.length);
|
|
14
|
+
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
15
|
+
for (let i = 0; i < input.length; i++) {
|
|
16
|
+
const x = input[i];
|
|
17
|
+
const y = b[0] * x + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
|
|
18
|
+
x2 = x1; x1 = x; y2 = y1; y1 = y;
|
|
19
|
+
out[i] = y;
|
|
20
|
+
}
|
|
21
|
+
return out;
|
|
22
|
+
}
|
|
23
|
+
export function integratedLoudness(channels, rate = RATE) {
|
|
24
|
+
if (rate !== RATE) throw new Error("the loudness meter's K-weighting coefficients are for 48 kHz");
|
|
25
|
+
const weighted = channels.map((channel) => biquad(biquad(channel, shelf), highPass));
|
|
26
|
+
const block = Math.round(0.4 * rate), hop = Math.round(0.1 * rate);
|
|
27
|
+
const blocks = [];
|
|
28
|
+
for (let start = 0; start + block <= weighted[0].length; start += hop) {
|
|
29
|
+
let sum = 0;
|
|
30
|
+
for (const channel of weighted) { let s = 0; for (let i = start; i < start + block; i++) s += channel[i] * channel[i]; sum += s / block; }
|
|
31
|
+
blocks.push(sum);
|
|
32
|
+
}
|
|
33
|
+
const loudness = (z) => -0.691 + 10 * Math.log10(z);
|
|
34
|
+
const absolute = blocks.filter((z) => loudness(z) > -70);
|
|
35
|
+
if (absolute.length === 0) return -Infinity;
|
|
36
|
+
const relativeGate = loudness(absolute.reduce((s, z) => s + z, 0) / absolute.length) - 10;
|
|
37
|
+
const gated = absolute.filter((z) => loudness(z) > relativeGate);
|
|
38
|
+
return loudness(gated.reduce((s, z) => s + z, 0) / gated.length);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const dbfs = (value) => (value > 0 ? 20 * Math.log10(value) : -Infinity);
|
|
42
|
+
export function samplePeakDbfs(channels) {
|
|
43
|
+
let peak = 0;
|
|
44
|
+
for (const channel of channels) for (const v of channel) peak = Math.max(peak, Math.abs(v));
|
|
45
|
+
return dbfs(peak);
|
|
46
|
+
}
|
|
47
|
+
export function wavBytes(pcm, rate = RATE, channels = 2) {
|
|
48
|
+
const data = Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
49
|
+
const header = Buffer.alloc(44);
|
|
50
|
+
header.write("RIFF", 0); header.writeUInt32LE(36 + data.length, 4); header.write("WAVEfmt ", 8);
|
|
51
|
+
header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(rate, 24);
|
|
52
|
+
header.writeUInt32LE(rate * channels * 2, 28); header.writeUInt16LE(channels * 2, 32); header.writeUInt16LE(16, 34); header.write("data", 36); header.writeUInt32LE(data.length, 40);
|
|
53
|
+
return Buffer.concat([header, data]);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// A lookahead limiter: the gain that keeps every sample under `ceiling` is found 5 ms ahead, so the
|
|
57
|
+
// reduction is fully in place when the peak arrives, then released over 80 ms. Returns new channels.
|
|
58
|
+
export function limit(channels, ceiling, { lookaheadSec = 0.005, releaseSec = 0.08, rate = RATE } = {}) {
|
|
59
|
+
const n = channels[0].length;
|
|
60
|
+
const look = Math.max(1, Math.round(lookaheadSec * rate));
|
|
61
|
+
const need = new Float64Array(n);
|
|
62
|
+
for (let i = 0; i < n; i++) {
|
|
63
|
+
let peak = 0;
|
|
64
|
+
for (const channel of channels) peak = Math.max(peak, Math.abs(channel[i]));
|
|
65
|
+
need[i] = peak > ceiling ? ceiling / peak : 1;
|
|
66
|
+
}
|
|
67
|
+
// The minimum of the required gain over the next `look` samples (a sliding window, monotone deque).
|
|
68
|
+
const floor = new Float64Array(n);
|
|
69
|
+
const deque = [];
|
|
70
|
+
let head = 0;
|
|
71
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
72
|
+
while (deque.length > head && need[deque.at(-1)] >= need[i]) deque.pop();
|
|
73
|
+
deque.push(i);
|
|
74
|
+
while (deque[head] > i + look) head++;
|
|
75
|
+
floor[i] = need[deque[head]];
|
|
76
|
+
}
|
|
77
|
+
const release = Math.exp(-1 / (releaseSec * rate));
|
|
78
|
+
const attack = Math.exp(-1 / (look * 0.5));
|
|
79
|
+
const gain = new Float64Array(n);
|
|
80
|
+
let g = 1;
|
|
81
|
+
for (let i = 0; i < n; i++) {
|
|
82
|
+
g = floor[i] < g ? attack * g + (1 - attack) * floor[i] : release * g + (1 - release) * floor[i];
|
|
83
|
+
gain[i] = g;
|
|
84
|
+
}
|
|
85
|
+
// The smoothed gain can sit above the required gain for a few samples on the attack; clamp so no sample passes the ceiling.
|
|
86
|
+
return channels.map((channel) => channel.map((v, i) => v * Math.min(gain[i], need[i])));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// The master stage: gain to -16 LUFS, then the limiter under -2 dBFS, repeated until the limited result
|
|
90
|
+
// sits within 0.1 LU. Returns the final channels, the gain applied before the limiter and the loudness.
|
|
91
|
+
export function masterTo(left, right, { target = TARGET_LUFS, ceilingDbfs = PEAK_CEILING_DBFS } = {}) {
|
|
92
|
+
const ceiling = 10 ** (ceilingDbfs / 20) * 0.995;
|
|
93
|
+
let gain = 1, lufs = integratedLoudness([left, right]), out = [left, right];
|
|
94
|
+
for (let pass = 0; pass < 8 && Number.isFinite(lufs); pass++) {
|
|
95
|
+
gain *= 10 ** ((target - lufs) / 20);
|
|
96
|
+
out = limit([left.map((v) => v * gain), right.map((v) => v * gain)], ceiling);
|
|
97
|
+
lufs = integratedLoudness(out);
|
|
98
|
+
if (Math.abs(lufs - target) < 0.1) break;
|
|
99
|
+
}
|
|
100
|
+
return { left: out[0], right: out[1], gain, lufs };
|
|
101
|
+
}
|