@litfamily/litopencode 1.0.31 → 1.0.33
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 +38 -0
- package/README-Ko-KR.md +6 -6
- package/README.md +6 -6
- package/TRADEMARK.md +12 -0
- package/dist/activation-primary-prompts.js +5 -1
- 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/agents/defaults.js +1 -1
- 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 +58 -5
- package/dist/durable-plan.d.ts +5 -1
- package/dist/durable-plan.js +14 -2
- package/dist/features.d.ts +80 -1
- package/dist/features.js +105 -1
- package/dist/hooks.js +3 -0
- package/dist/ledger.js +12 -1
- package/dist/plan-scope-roots.d.ts +23 -0
- package/dist/plan-scope-roots.js +116 -0
- package/dist/restart-notice.d.ts +7 -0
- package/dist/restart-notice.js +40 -0
- package/dist/secret-shapes.js +5 -5
- package/dist/server.js +4 -0
- package/dist/session-liveness.d.ts +20 -0
- package/dist/session-liveness.js +99 -0
- 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 +167 -0
- package/dist/state.d.ts +3 -0
- package/dist/state.js +3 -0
- package/dist/tools.js +22 -2
- package/dist/tui/mission-band.d.ts +22 -1
- package/dist/tui/mission-band.js +75 -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 +49 -0
- package/dist/tui/mission-source.js +217 -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 +58 -0
- package/dist/tui/mission-threads.js +128 -0
- package/dist/tui.js +112 -7
- package/dist/uiux-visual-catalog.js +8 -1
- package/dist/v2/events.js +3 -0
- 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 +48 -3
- package/docs/reference.md +54 -3
- 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 +209 -0
- package/skills/lit-adhd/references/brief-format.md +46 -0
- package/skills/lit-adhd/references/card-examples.md +103 -0
- package/skills/lit-adhd/references/status-taxonomy.md +34 -0
- package/skills/lit-adhd/scripts/rewind.mjs +933 -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 +8 -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/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 +517 -88
- package/skills/readme-studio/SKILL.md +1 -1
- 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 +5 -2
- package/tools/payload-substance-parity.json +4 -3
- package/tools/qa-cover-recipes.mjs +4 -3
- package/tools/version-manifests.json +2 -2
|
@@ -7,53 +7,16 @@
|
|
|
7
7
|
import { spawnSync } from "node:child_process";
|
|
8
8
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import { PEAK_CEILING_DBFS, RATE, TARGET_LUFS, dbfs, integratedLoudness, samplePeakDbfs, wavBytes } from "./loudness.mjs";
|
|
11
|
+
import { accentsFor } from "./accents.mjs";
|
|
12
|
+
import { writeScore } from "./score.mjs";
|
|
10
13
|
import { mulberry32, fnv1a32 } from "./text.mjs";
|
|
11
14
|
|
|
15
|
+
// The meter, the sample peak and the WAV writer live in loudness.mjs; the gate and the tests still read them here.
|
|
16
|
+
export { RATE, accentsFor, integratedLoudness, samplePeakDbfs, wavBytes };
|
|
12
17
|
export const SOUND_EXIT = 20;
|
|
13
|
-
export const RATE = 48000;
|
|
14
|
-
const TARGET_LUFS = -16;
|
|
15
|
-
const PEAK_CEILING_DBFS = -2;
|
|
16
18
|
const fadeSec = 0.05;
|
|
17
19
|
|
|
18
|
-
// ---- ITU-R BS.1770-4: K-weighting at 48 kHz, 400 ms blocks with 75 % overlap, -70 LUFS absolute and
|
|
19
|
-
// -10 LU relative gates ----
|
|
20
|
-
const shelf = { b: [1.53512485958697, -2.69169618940638, 1.19839281085285], a: [1, -1.69065929318241, 0.73248077421585] };
|
|
21
|
-
const highPass = { b: [1, -2, 1], a: [1, -1.99004745483398, 0.99007225036621] };
|
|
22
|
-
function biquad(input, { b, a }) {
|
|
23
|
-
const out = new Float64Array(input.length);
|
|
24
|
-
let x1 = 0, x2 = 0, y1 = 0, y2 = 0;
|
|
25
|
-
for (let i = 0; i < input.length; i++) {
|
|
26
|
-
const x = input[i];
|
|
27
|
-
const y = b[0] * x + b[1] * x1 + b[2] * x2 - a[1] * y1 - a[2] * y2;
|
|
28
|
-
x2 = x1; x1 = x; y2 = y1; y1 = y;
|
|
29
|
-
out[i] = y;
|
|
30
|
-
}
|
|
31
|
-
return out;
|
|
32
|
-
}
|
|
33
|
-
export function integratedLoudness(channels, rate = RATE) {
|
|
34
|
-
if (rate !== RATE) throw new Error("the loudness meter's K-weighting coefficients are for 48 kHz");
|
|
35
|
-
const weighted = channels.map((channel) => biquad(biquad(channel, shelf), highPass));
|
|
36
|
-
const block = Math.round(0.4 * rate), hop = Math.round(0.1 * rate);
|
|
37
|
-
const blocks = [];
|
|
38
|
-
for (let start = 0; start + block <= weighted[0].length; start += hop) {
|
|
39
|
-
let sum = 0;
|
|
40
|
-
for (const channel of weighted) { let s = 0; for (let i = start; i < start + block; i++) s += channel[i] * channel[i]; sum += s / block; }
|
|
41
|
-
blocks.push(sum);
|
|
42
|
-
}
|
|
43
|
-
const loudness = (z) => -0.691 + 10 * Math.log10(z);
|
|
44
|
-
const absolute = blocks.filter((z) => loudness(z) > -70);
|
|
45
|
-
if (absolute.length === 0) return -Infinity;
|
|
46
|
-
const relativeGate = loudness(absolute.reduce((s, z) => s + z, 0) / absolute.length) - 10;
|
|
47
|
-
const gated = absolute.filter((z) => loudness(z) > relativeGate);
|
|
48
|
-
return loudness(gated.reduce((s, z) => s + z, 0) / gated.length);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const dbfs = (value) => (value > 0 ? 20 * Math.log10(value) : -Infinity);
|
|
52
|
-
export function samplePeakDbfs(channels) {
|
|
53
|
-
let peak = 0;
|
|
54
|
-
for (const channel of channels) for (const v of channel) peak = Math.max(peak, Math.abs(v));
|
|
55
|
-
return dbfs(peak);
|
|
56
|
-
}
|
|
57
20
|
// The longest run of 50 ms windows under `floorDb` RMS inside [0, untilSec).
|
|
58
21
|
export function quietestStretchSec(channels, rate, untilSec = 3, floorDb = -50) {
|
|
59
22
|
const win = Math.round(0.05 * rate), end = Math.min(channels[0].length, Math.round(untilSec * rate));
|
|
@@ -89,12 +52,6 @@ const palettes = {
|
|
|
89
52
|
air: { pad: (f, t) => 0.5 * Math.sin(2 * Math.PI * f * t) + 0.12 * Math.sin(2 * Math.PI * 1.5 * f * t), pulse: 0.25, noise: 0.08 }
|
|
90
53
|
};
|
|
91
54
|
|
|
92
|
-
// Accent words a beat's `sound` field may carry.
|
|
93
|
-
export function accentsFor(beat) {
|
|
94
|
-
const text = String(beat.sound ?? "").toLowerCase();
|
|
95
|
-
return { hit: /\bhits?\b|\bimpact\b|\bboom\b|\bstab\b/u.test(text), rise: /\brise\b|\briser\b|\bswell\b|\bbuild\b/u.test(text), cadence: /\bcadence\b|\bresolve\b|\bfinal chord\b/u.test(text), thin: /\bthin(?:s|ned)?\b|\bsparse\b|\bpared back\b/u.test(text) };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
55
|
// cuts[i] is the film time where beat i starts on screen (the render's own cut times).
|
|
99
56
|
export function buildBed(treatment, { frameCount, fps, cuts }) {
|
|
100
57
|
const sound = treatment.sound;
|
|
@@ -207,15 +164,6 @@ export function buildBed(treatment, { frameCount, fps, cuts }) {
|
|
|
207
164
|
return { pcm, cues, stats: { samples: length, durationSec: duration, lufs: Number(integratedLoudness(decoded).toFixed(2)), peakDbfs: Number(samplePeakDbfs(decoded).toFixed(2)), palette: sound.palette, key: sound.key, tempo: sound.tempo } };
|
|
208
165
|
}
|
|
209
166
|
|
|
210
|
-
export function wavBytes(pcm, rate = RATE, channels = 2) {
|
|
211
|
-
const data = Buffer.from(pcm.buffer, pcm.byteOffset, pcm.byteLength);
|
|
212
|
-
const header = Buffer.alloc(44);
|
|
213
|
-
header.write("RIFF", 0); header.writeUInt32LE(36 + data.length, 4); header.write("WAVEfmt ", 8);
|
|
214
|
-
header.writeUInt32LE(16, 16); header.writeUInt16LE(1, 20); header.writeUInt16LE(channels, 22); header.writeUInt32LE(rate, 24);
|
|
215
|
-
header.writeUInt32LE(rate * channels * 2, 28); header.writeUInt16LE(channels * 2, 32); header.writeUInt16LE(16, 34); header.write("data", 36); header.writeUInt32LE(data.length, 40);
|
|
216
|
-
return Buffer.concat([header, data]);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
167
|
// Writes <out>/sound.wav and <out>/sound-cues.json for a generated bed; returns the stats.
|
|
220
168
|
export function writeBed(out, treatment, timing) {
|
|
221
169
|
const bed = buildBed(treatment, timing);
|
|
@@ -230,6 +178,10 @@ export function resolveTrack(out, treatment, timing, briefAudio) {
|
|
|
230
178
|
const mode = treatment.sound.mode;
|
|
231
179
|
if (mode === "none") return { mode };
|
|
232
180
|
if (mode === "generated") return { mode, ...writeBed(out, treatment, timing), label: "generated" };
|
|
181
|
+
if (mode === "score") {
|
|
182
|
+
const summary = writeScore({ treatment, frameCount: timing.frameCount, fps: timing.fps, cuts: timing.cuts, dir: out });
|
|
183
|
+
return { mode, file: summary.file, label: "score", lufs: summary.lufs, peakDbfs: summary.peakDbfs, key: summary.key, tempo: summary.tempo, palette: summary.genre };
|
|
184
|
+
}
|
|
233
185
|
const named = treatment.sound.file ?? briefAudio;
|
|
234
186
|
const file = named ? path.resolve(out, named) : null;
|
|
235
187
|
if (!file || !existsSync(file)) throw Object.assign(new Error(`SOUND_INVALID (20): the ${mode} track ${named ?? "(none named)"} is missing`), { exitCode: SOUND_EXIT });
|
|
@@ -12,12 +12,18 @@ export const stageHost = "lit.stage";
|
|
|
12
12
|
export const stageOrigin = `http://${stageHost}`;
|
|
13
13
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
14
14
|
const kitSource = () => readFileSync(path.join(here, "stage-kit.js"), "utf8");
|
|
15
|
+
const threeRoot = path.join(here, "..", "vendor", "three");
|
|
15
16
|
const clockSource = () => readFileSync(path.join(here, "clock.js"), "utf8");
|
|
16
17
|
|
|
17
|
-
// The
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
// The stage's Chrome flags on one rung. "swiftshader" (the default) is the software rung: CPU raster,
|
|
19
|
+
// the same pixels on every machine. "gpu" asks the platform's native ANGLE backend for WebGL, so a
|
|
20
|
+
// three.js film draws in real time on the graphics card; page raster stays on the CPU, which keeps the
|
|
21
|
+
// text and the capture path identical to the software rung.
|
|
22
|
+
export function stageChromeFlags(width, height, rung = "swiftshader", platform = process.platform) {
|
|
23
|
+
const found = chromeFlagRungs(platform).find((candidate) => candidate.name === rung);
|
|
24
|
+
if (!found) throw new Error(`unknown Chrome rung "${rung}" (gpu or swiftshader)`);
|
|
25
|
+
const base = rung === "gpu" ? [...found.flags.filter((flag) => flag !== "--enable-gpu-rasterization"), "--disable-gpu-rasterization"] : found.flags;
|
|
26
|
+
return [...base,
|
|
21
27
|
"--run-all-compositor-stages-before-draw", "--disable-checker-imaging", "--disable-new-content-rendering-timeout",
|
|
22
28
|
"--disable-threaded-animation", "--disable-threaded-scrolling", "--disable-image-animation-resync", "--disable-lcd-text",
|
|
23
29
|
"--force-color-profile=srgb", "--hide-scrollbars", "--mute-audio", "--force-device-scale-factor=1", `--window-size=${width},${height}`,
|
|
@@ -47,7 +53,7 @@ export function stageFonts(skillRoot, state, pins) {
|
|
|
47
53
|
}
|
|
48
54
|
|
|
49
55
|
// Serve the stage origin from disk through request interception: the product's scripts and faces
|
|
50
|
-
// under /lit
|
|
56
|
+
// under /lit/ (fonts, and the pinned three.js under /lit/three/), every other path from inside `stageDir` only. Anything else is recorded, never fetched:
|
|
51
57
|
// another origin in `blocked`, a path that leaves the directory or has no stage type in `refused`, a
|
|
52
58
|
// file that is not there in `missing`. The stage and the cover browsers both use it.
|
|
53
59
|
export async function serveStage(context, { stageDir, fonts, scripts }) {
|
|
@@ -59,12 +65,19 @@ export async function serveStage(context, { stageDir, fonts, scripts }) {
|
|
|
59
65
|
const url = new URL(route.request().url());
|
|
60
66
|
if (url.origin !== stageOrigin) { blocked.push(url.href); return route.abort("blockedbyclient"); }
|
|
61
67
|
const pathname = decodeURIComponent(url.pathname);
|
|
62
|
-
if (Object.hasOwn(scripts, pathname)) return route.fulfill({ status: 200, contentType: contentTypes[".js"], body: scripts[pathname] });
|
|
68
|
+
if (Object.hasOwn(scripts, pathname)) return route.fulfill({ status: 200, contentType: contentTypes[path.extname(pathname)] ?? contentTypes[".js"], body: scripts[pathname] });
|
|
63
69
|
if (pathname === "/lit/fonts.css") return route.fulfill({ status: 200, contentType: contentTypes[".css"], body: fonts.css });
|
|
64
70
|
if (pathname.startsWith("/lit/fonts/")) {
|
|
65
71
|
const file = fonts.files.get(pathname.slice("/lit/fonts/".length));
|
|
66
72
|
return file && existsSync(file) ? route.fulfill({ status: 200, contentType: contentTypes[path.extname(file)], body: readFileSync(file) }) : route.fulfill({ status: 404, body: "" });
|
|
67
73
|
}
|
|
74
|
+
if (pathname.startsWith("/lit/three/")) {
|
|
75
|
+
// The pinned three.js build and its two addons, JavaScript only; the licence and anything else is not served.
|
|
76
|
+
const sub = pathname.slice("/lit/three/".length);
|
|
77
|
+
const file = path.join(threeRoot, sub);
|
|
78
|
+
const ok = !sub.split("/").includes("..") && sub.endsWith(".js") && insideDir(threeRoot, file) && existsSync(file);
|
|
79
|
+
return ok ? route.fulfill({ status: 200, contentType: contentTypes[".js"], body: readFileSync(file) }) : route.fulfill({ status: 404, body: "" });
|
|
80
|
+
}
|
|
68
81
|
const rel = pathname.replace(/^\/+/u, "") || "index.html";
|
|
69
82
|
if (rel.split("/").includes("..")) { refused.push(rel); return route.fulfill({ status: 404, body: "" }); }
|
|
70
83
|
const full = path.join(root, rel);
|
|
@@ -79,10 +92,11 @@ export async function serveStage(context, { stageDir, fonts, scripts }) {
|
|
|
79
92
|
}
|
|
80
93
|
|
|
81
94
|
// Launch one stage browser. `profile` is a short directory under the run's .run/, removed on close.
|
|
82
|
-
|
|
95
|
+
// `styleFiles` maps /lit/style.css and /lit/style.js to the film's house style, written per treatment.
|
|
96
|
+
export async function openStage(chromium, { chromePath, profile, stageDir, width, height, fps, seed, fonts, extraInit, styleFiles = {}, rung = "swiftshader" }) {
|
|
83
97
|
rmSync(profile, { recursive: true, force: true });
|
|
84
98
|
mkdirSync(profile, { recursive: true });
|
|
85
|
-
const flags = stageChromeFlags(width, height);
|
|
99
|
+
const flags = stageChromeFlags(width, height, rung);
|
|
86
100
|
let context;
|
|
87
101
|
try {
|
|
88
102
|
context = await chromium.launchPersistentContext(profile, {
|
|
@@ -94,7 +108,7 @@ export async function openStage(chromium, { chromePath, profile, stageDir, width
|
|
|
94
108
|
const line = String(error?.message ?? error).split("\n").find((part) => part.trim()) ?? String(error);
|
|
95
109
|
throw Object.assign(new Error(`BLOCKED_NO_CHROME (10): Chrome failed to launch headless: ${line}; fix: check that Chrome can start headless in this sandbox`), { exitCode: 10 });
|
|
96
110
|
}
|
|
97
|
-
const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource() } });
|
|
111
|
+
const { blocked, refused, missing } = await serveStage(context, { stageDir, fonts, scripts: { "/lit/stage-kit.js": kitSource(), ...styleFiles } });
|
|
98
112
|
await context.addInitScript({ content: `window.__litStageConfig = ${JSON.stringify({ seed: seed >>> 0, fps })};\n${clockSource()}` });
|
|
99
113
|
if (extraInit) await context.addInitScript({ content: extraInit });
|
|
100
114
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -65,12 +65,18 @@
|
|
|
65
65
|
new MutationObserver((list) => { for (const m of list) for (const node of m.addedNodes) inspect(node); }).observe(document, { childList: true, subtree: true });
|
|
66
66
|
|
|
67
67
|
// ---- canvas: WebGL keeps its drawing buffer for capture; 2D text calls are listed for the QA ----
|
|
68
|
-
const webgl = { requested: false, failed: false };
|
|
68
|
+
const webgl = { requested: false, failed: false, renderer: null };
|
|
69
69
|
const nativeGetContext = HTMLCanvasElement.prototype.getContext;
|
|
70
70
|
HTMLCanvasElement.prototype.getContext = function (type, attributes, ...rest) {
|
|
71
71
|
const isGl = /webgl/iu.test(String(type));
|
|
72
72
|
const ctx = nativeGetContext.call(this, type, isGl ? { ...(attributes ?? {}), preserveDrawingBuffer: true } : attributes, ...rest);
|
|
73
|
-
if (isGl) {
|
|
73
|
+
if (isGl) {
|
|
74
|
+
webgl.requested = true;
|
|
75
|
+
if (!ctx) webgl.failed = true;
|
|
76
|
+
else if (!webgl.renderer) {
|
|
77
|
+
try { const info = ctx.getExtension("WEBGL_debug_renderer_info"); webgl.renderer = info ? String(ctx.getParameter(info.UNMASKED_RENDERER_WEBGL)) : String(ctx.getParameter(ctx.RENDERER)); } catch { webgl.renderer = null; }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
74
80
|
return ctx;
|
|
75
81
|
};
|
|
76
82
|
const canvasTexts = new Set();
|
|
@@ -23,7 +23,18 @@
|
|
|
23
23
|
CoverDate.UTC = RealDate.UTC;
|
|
24
24
|
Object.defineProperty(window, "Date", { value: CoverDate, writable: true, configurable: true });
|
|
25
25
|
Object.defineProperty(Performance.prototype, "now", { value: refuse("performance.now", fromT), writable: true, configurable: true });
|
|
26
|
-
|
|
26
|
+
// Building a scene before window.ready may draw ids from Math.random (three.js names every object it
|
|
27
|
+
// makes): that phase gets a fixed-seed sequence, the same in every browser. Once the page is ready,
|
|
28
|
+
// frames are drawn in any order, so a call then is misuse.
|
|
29
|
+
const refusedRandom = refuse("Math.random", "use LitLoop.rng(seed) for layout and t for motion");
|
|
30
|
+
let seed = 0x9e3779b9;
|
|
31
|
+
Math.random = function random() {
|
|
32
|
+
if (window.ready === true) return refusedRandom();
|
|
33
|
+
seed = (seed + 0x6d2b79f5) | 0;
|
|
34
|
+
let x = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
|
35
|
+
x = (x + Math.imul(x ^ (x >>> 7), 61 | x)) ^ x;
|
|
36
|
+
return ((x ^ (x >>> 14)) >>> 0) / 4294967296;
|
|
37
|
+
};
|
|
27
38
|
window.requestAnimationFrame = refuse("requestAnimationFrame", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
|
|
28
39
|
window.setInterval = refuse("setInterval", "the renderer calls renderFrame(t); a cover page runs no loop of its own");
|
|
29
40
|
if (window.crypto) Object.defineProperty(window.crypto, "getRandomValues", { value: refuse("crypto.getRandomValues", "use LitLoop.rng(seed)"), configurable: true });
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { createHash } from "node:crypto";
|
|
5
5
|
import { parentPort } from "node:worker_threads";
|
|
6
6
|
import { auditCells, flashGeometryFor, linearLut } from "../flash.mjs";
|
|
7
|
+
import { busyShare } from "../fill.mjs";
|
|
7
8
|
import { areaResize, decodePng, encodePng } from "../png.mjs";
|
|
8
9
|
|
|
9
10
|
function toRgba(image) {
|
|
@@ -16,14 +17,20 @@ function toRgba(image) {
|
|
|
16
17
|
return out;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
|
-
//
|
|
20
|
-
|
|
20
|
+
// The 0.5 % and 99.5 % points of linear luminance. MO-D-03 reads the upper one (near-black), and
|
|
21
|
+
// FLAT-FRAME reads the spread between them: a frame of one flat colour, light or dark, has almost none.
|
|
22
|
+
function spread(rgba) {
|
|
21
23
|
const bins = new Uint32Array(1024);
|
|
22
24
|
for (let i = 0; i < rgba.length; i += 4) bins[Math.min(1023, Math.floor((0.2126 * linearLut[rgba[i]] + 0.7152 * linearLut[rgba[i + 1]] + 0.0722 * linearLut[rgba[i + 2]]) * 1024))]++;
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
const total = rgba.length / 4;
|
|
26
|
+
const high = total * 0.995, low = total * 0.005;
|
|
27
|
+
let acc = 0, p005 = null, p995 = 1;
|
|
28
|
+
for (let b = 0; b < 1024; b++) {
|
|
29
|
+
acc += bins[b];
|
|
30
|
+
if (p005 === null && acc >= low) p005 = b / 1024;
|
|
31
|
+
if (acc >= high) { p995 = (b + 1) / 1024; break; }
|
|
32
|
+
}
|
|
33
|
+
return { p005: p005 ?? 0, p995 };
|
|
27
34
|
}
|
|
28
35
|
|
|
29
36
|
parentPort.on("message", ({ id, png, width, height, preview }) => {
|
|
@@ -36,7 +43,8 @@ parentPort.on("message", ({ id, png, width, height, preview }) => {
|
|
|
36
43
|
const rgba = toRgba(image);
|
|
37
44
|
const cells = auditCells(rgba, width, height, flashGeometryFor(width, height));
|
|
38
45
|
const hash = createHash("sha256").update(rgba).digest("hex");
|
|
39
|
-
const
|
|
46
|
+
const { p005, p995 } = spread(rgba);
|
|
47
|
+
const out = { id, hash, p995L: Number(p995.toFixed(5)), p005L: Number(p005.toFixed(5)), busy: Number(busyShare(rgba, width, height).toFixed(4)), cells };
|
|
40
48
|
if (preview) out.previewPng = encodePng(preview.width, preview.height, areaResize(rgba, width, height, preview.width, preview.height, 4), 4, 1);
|
|
41
49
|
const copy = rgba.byteOffset === 0 && rgba.byteLength === rgba.buffer.byteLength ? new Uint8Array(rgba.buffer) : Uint8Array.from(rgba);
|
|
42
50
|
out.rgba = copy;
|
|
@@ -11,6 +11,47 @@ import { soundChecks } from "../sound.mjs";
|
|
|
11
11
|
|
|
12
12
|
const pass = (detail = "") => ({ pass: true, detail });
|
|
13
13
|
const fail = (detail) => ({ pass: false, detail });
|
|
14
|
+
const notMeasured = (detail) => ({ pass: true, na: true, detail });
|
|
15
|
+
|
|
16
|
+
// The empty-frame rules. A frame of one flat colour (0.5-99.5 % luminance spread under 0.02) may hold
|
|
17
|
+
// for a second (a second more at the open or the close); a frame where fewer than 2.5 % of its 48 px
|
|
18
|
+
// cells carry anything may hold for 0.5 s, with 0.4 s more to build at the open and 0.6 s more to
|
|
19
|
+
// fade at the close. Measured on earlier films, a known dead frame sat at 1.1 % and drawn shots at 5 %
|
|
20
|
+
// and up.
|
|
21
|
+
export const stageGate = Object.freeze({ flatSpread: 0.02, flatHoldSec: 1, flatEdgeSec: 1, deadBusy: 0.025, deadHoldSec: 0.5, deadOpenSec: 0.4, deadCloseSec: 0.6 });
|
|
22
|
+
|
|
23
|
+
// The longest run of frames where `hit` holds that exceeds its allowance, or null.
|
|
24
|
+
function longestRun(count, hit, limit, allowance) {
|
|
25
|
+
let worst = null;
|
|
26
|
+
let start = -1;
|
|
27
|
+
for (let f = 0; f <= count; f++) {
|
|
28
|
+
if (f < count && hit(f)) {
|
|
29
|
+
if (start < 0) start = f;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (start >= 0) {
|
|
33
|
+
const length = f - start;
|
|
34
|
+
if (length > limit + allowance(start === 0, f === count) && (!worst || length > worst.length)) worst = { length, from: start };
|
|
35
|
+
start = -1;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return worst;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function flatFrames(frameLines, fps) {
|
|
42
|
+
if (!frameLines.length || frameLines.some((line) => typeof line.p005L !== "number")) return notMeasured("per-frame luminance spread not recorded");
|
|
43
|
+
const flat = frameLines.map((line) => line.p995L - line.p005L < stageGate.flatSpread);
|
|
44
|
+
const edge = stageGate.flatEdgeSec * fps;
|
|
45
|
+
const worst = longestRun(flat.length, (f) => flat[f], stageGate.flatHoldSec * fps, (open, close) => (open || close ? edge : 0));
|
|
46
|
+
return worst ? fail(`${(worst.length / fps).toFixed(2)} s of empty frames from ${(worst.from / fps).toFixed(2)} s (nothing but one flat colour; limit ${stageGate.flatHoldSec} s)`) : pass(`no run of empty frames longer than ${stageGate.flatHoldSec} s`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function deadAir(frameLines, fps) {
|
|
50
|
+
if (!frameLines.length || frameLines.some((line) => typeof line.busy !== "number")) return notMeasured("per-frame busy share not recorded");
|
|
51
|
+
const dead = frameLines.map((line) => line.busy < stageGate.deadBusy);
|
|
52
|
+
const worst = longestRun(dead.length, (f) => dead[f], stageGate.deadHoldSec * fps, (open, close) => (open ? stageGate.deadOpenSec * fps : 0) + (close ? stageGate.deadCloseSec * fps : 0));
|
|
53
|
+
return worst ? fail(`${(worst.length / fps).toFixed(2)} s of near-empty frames from ${(worst.from / fps).toFixed(2)} s (under ${stageGate.deadBusy * 100} % of the frame carries anything; limit ${stageGate.deadHoldSec} s)`) : pass(`no near-empty run longer than ${stageGate.deadHoldSec} s`);
|
|
54
|
+
}
|
|
14
55
|
|
|
15
56
|
function probe(file) {
|
|
16
57
|
const result = spawnSync("ffprobe", ["-v", "error", "-show_entries", "stream=codec_type,codec_name,width,height,r_frame_rate,pix_fmt,color_range,color_space,color_transfer,color_primaries,duration,sample_rate,channels:format=duration", "-of", "json", file], { encoding: "utf8" });
|
|
@@ -41,7 +82,7 @@ export function checkStageRun(root) {
|
|
|
41
82
|
: !previewLines.length ? fail(`preview frames were not audited; ${flashDetail}`)
|
|
42
83
|
: general <= limits.flashLimitGeneral && red <= limits.flashLimitRed ? pass(flashDetail) : fail(flashDetail);
|
|
43
84
|
|
|
44
|
-
if (manifest.webgl?.requested) checks["MO-C-02"] = manifest.webgl.failed ? fail("the page asked for WebGL and got none") : pass("WebGL on the stage's software rung (SwiftShader), labelled software");
|
|
85
|
+
if (manifest.webgl?.requested) checks["MO-C-02"] = manifest.webgl.failed ? fail("the page asked for WebGL and got none") : pass(manifest.rung === "gpu" ? `WebGL on the GPU rung (${manifest.renderer ?? "native ANGLE"})` : `WebGL on the stage's software rung (SwiftShader), labelled software${manifest.warnings?.length ? "; the 3D route fell back here, see the notice" : ""}`);
|
|
45
86
|
|
|
46
87
|
const det = readJson(path.join(root, ".run", "determinism.json"));
|
|
47
88
|
const mismatch = det?.frames?.find((entry) => entry.master !== entry.replay);
|
|
@@ -82,6 +123,8 @@ export function checkStageRun(root) {
|
|
|
82
123
|
if (run > allowance + edge) { emptyError = `${run} near-black frames ending at frame ${line.frame}`; break; }
|
|
83
124
|
}
|
|
84
125
|
checks["MO-D-03"] = emptyError ? fail(emptyError) : pass(`no near-black run past ${allowance} frames`);
|
|
126
|
+
checks["FLAT-FRAME"] = flatFrames(frameLines, fps);
|
|
127
|
+
checks["DEAD-AIR"] = deadAir(frameLines, fps);
|
|
85
128
|
|
|
86
129
|
if (manifest.sound?.mode) Object.assign(checks, soundChecks(film, manifest.durationSec, manifest.sound.mode));
|
|
87
130
|
const qa = readJson(path.join(root, ".run", "qa.json"));
|
|
@@ -94,7 +137,7 @@ export function checkStageRun(root) {
|
|
|
94
137
|
return { manifest, checks, warnings, failed, passed: failed.length === 0, sizes, flash: { master, preview } };
|
|
95
138
|
}
|
|
96
139
|
|
|
97
|
-
const order = ["MO-C-03", "MO-C-02", "MO-C-09", "MO-C-10", "MO-C-11", "MO-C-12", "MO-C-13", "MO-C-14", "MO-D-03"];
|
|
140
|
+
const order = ["MO-C-03", "MO-C-02", "MO-C-09", "MO-C-10", "MO-C-11", "MO-C-12", "MO-C-13", "MO-C-14", "MO-D-03", "FLAT-FRAME", "DEAD-AIR"];
|
|
98
141
|
export function formatStageReport(result, context) {
|
|
99
142
|
const { manifest, checks } = result;
|
|
100
143
|
const out = (name) => path.join(context.exportDir, name);
|
|
@@ -102,7 +145,8 @@ export function formatStageReport(result, context) {
|
|
|
102
145
|
"lit-typographic-motion — render report (stage)",
|
|
103
146
|
`outputs: ${out("film.mp4")} · ${out(manifest.previewEncoder === "gif" ? "preview.gif" : "preview.webp")} (encoder: ${manifest.previewEncoder}) · ${out("poster.png")} · ${out("reduced-motion.png")}`,
|
|
104
147
|
`format / fps / length: ${manifest.format} ${manifest.resolution.join("x")} @ ${manifest.fps} fps, ${manifest.durationSec.toFixed(2)} s (treatment ${manifest.targetDurationSec} s)`,
|
|
105
|
-
`renderer: stage software rung (flags: ${(manifest.chromeFlags ?? []).join(" ")})`,
|
|
148
|
+
`renderer: stage ${manifest.rung === "gpu" ? `GPU rung (${manifest.renderer ?? "native ANGLE"})` : "software rung"} (flags: ${(manifest.chromeFlags ?? []).join(" ")})`,
|
|
149
|
+
...(manifest.warnings ?? []).map((note) => `notice: ${note}`),
|
|
106
150
|
`timing: master ${manifest.timing?.masterSec ?? "?"} s, per frame p50 ${manifest.timing?.frameP50 ?? "?"} ms / p95 ${manifest.timing?.frameP95 ?? "?"} ms, total ${manifest.timing?.totalSec ?? "?"} s`,
|
|
107
151
|
"",
|
|
108
152
|
`QA gate: ${result.passed ? "PASS" : "FAIL"}`
|
|
@@ -110,7 +154,7 @@ export function formatStageReport(result, context) {
|
|
|
110
154
|
const ids = [...order.filter((id) => checks[id]), ...Object.keys(checks).filter((id) => !order.includes(id))];
|
|
111
155
|
for (const id of ids) {
|
|
112
156
|
const check = checks[id];
|
|
113
|
-
lines.push(` ${id} ${check.pass ? "PASS" : check.warn ? "WARN" : "FAIL"}${check.detail ? ` (${check.detail})` : ""}`);
|
|
157
|
+
lines.push(` ${id} ${check.na ? "N/A" : check.pass ? "PASS" : check.warn ? "WARN" : "FAIL"}${check.detail ? ` (${check.detail})` : ""}`);
|
|
114
158
|
}
|
|
115
159
|
lines.push("", `craft rounds run: ${manifest.craftRound} / 3 max`, `frames actually viewed this run: ${context.viewed} (confirmed looked, not just rendered)`);
|
|
116
160
|
if (result.warnings.length) lines.push(`warnings: ${result.warnings.join("; ")}`);
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
}
|
|
38
38
|
return out.map(([x0, y0, x1, y1]) => [Math.max(0, x0), Math.max(0, y0), Math.min(innerWidth, x1), Math.min(innerHeight, y1)]).filter(([x0, y0, x1, y1]) => x1 > x0 && y1 > y0);
|
|
39
39
|
}
|
|
40
|
+
const firstFamily = (style) => String(style.fontFamily).split(",")[0].trim().replace(/^["']|["']$/gu, "");
|
|
41
|
+
|
|
40
42
|
function runRoot(textNode) {
|
|
41
43
|
const parent = textNode.parentElement;
|
|
42
44
|
const marked = parent.closest("[data-lit-text]");
|
|
@@ -60,7 +62,7 @@
|
|
|
60
62
|
for (const [root, nodes] of groups) {
|
|
61
63
|
const range = document.createRange();
|
|
62
64
|
const rects = [], elements = new Set();
|
|
63
|
-
let opacity = 1, fontSize = 0, weight = 400, scale = 1, gradient = false;
|
|
65
|
+
let opacity = 1, fontSize = 0, weight = 400, scale = 1, gradient = false, color = "", family = "";
|
|
64
66
|
for (const node of nodes) {
|
|
65
67
|
range.selectNodeContents(node);
|
|
66
68
|
rects.push(...clipRects(node.parentElement, [...range.getClientRects()]));
|
|
@@ -69,11 +71,11 @@
|
|
|
69
71
|
opacity = Math.min(opacity, opacityChain(el));
|
|
70
72
|
const style = getComputedStyle(el);
|
|
71
73
|
const size = parseFloat(style.fontSize), s = scaleOf(el);
|
|
72
|
-
if (size * s > fontSize * scale) { fontSize = size; scale = s; weight = Number(style.fontWeight) || 400; }
|
|
74
|
+
if (size * s > fontSize * scale) { fontSize = size; scale = s; weight = Number(style.fontWeight) || 400; color = style.color; family = firstFamily(style); }
|
|
73
75
|
if (/text/u.test(style.webkitBackgroundClip || style.backgroundClip) || /url\(|gradient/u.test(style.fill)) gradient = true;
|
|
74
76
|
}
|
|
75
77
|
const text = nodes.map((node) => node.nodeValue).join("").replace(/\s+/gu, " ").trim();
|
|
76
|
-
runs.push({ text, norm: normalize(text), rects, opacity, fontSize, scale, weight, gradient, decor: root.closest("[data-lit-decor]") !== null, kind: root instanceof SVGElement ? "svg" : "dom", elements: [...elements] });
|
|
78
|
+
runs.push({ text, norm: normalize(text), rects, opacity, fontSize, scale, weight, gradient, color, family, decor: root.closest("[data-lit-decor]") !== null, kind: root instanceof SVGElement ? "svg" : "dom", elements: [...elements] });
|
|
77
79
|
}
|
|
78
80
|
for (const el of document.querySelectorAll("body *")) {
|
|
79
81
|
for (const pseudo of ["::before", "::after"]) {
|
|
@@ -82,7 +84,7 @@
|
|
|
82
84
|
const text = content.slice(1, -1).trim();
|
|
83
85
|
if (!text) continue;
|
|
84
86
|
const style = getComputedStyle(el, pseudo);
|
|
85
|
-
runs.push({ text, norm: normalize(text), rects: clipRects(el, [el.getBoundingClientRect()]), opacity: opacityChain(el), fontSize: parseFloat(style.fontSize), scale: scaleOf(el), weight: Number(style.fontWeight) || 400, gradient: false, decor: el.closest("[data-lit-decor]") !== null, kind: "pseudo", elements: [el] });
|
|
87
|
+
runs.push({ text, norm: normalize(text), rects: clipRects(el, [el.getBoundingClientRect()]), opacity: opacityChain(el), fontSize: parseFloat(style.fontSize), scale: scaleOf(el), weight: Number(style.fontWeight) || 400, gradient: false, color: style.color, family: firstFamily(style), decor: el.closest("[data-lit-decor]") !== null, kind: "pseudo", elements: [el] });
|
|
86
88
|
}
|
|
87
89
|
}
|
|
88
90
|
for (const entry of window.__litStage.registered()) {
|
|
@@ -147,7 +149,16 @@
|
|
|
147
149
|
probeFaces(families) {
|
|
148
150
|
const box = document.createElement("div");
|
|
149
151
|
box.style.cssText = "position:absolute;left:0;top:0;opacity:0;pointer-events:none";
|
|
150
|
-
|
|
152
|
+
// Each family at every weight and width the page can ask for: a static instance reports the file's own
|
|
153
|
+
// family name (Archivo at 900 is "Archivo Black"), so one default-weight probe would call it foreign.
|
|
154
|
+
let i = 0;
|
|
155
|
+
for (const family of families) for (const weight of [400, 700, 900]) for (const stretch of [75, 100, 125]) {
|
|
156
|
+
const el = document.createElement("span");
|
|
157
|
+
el.dataset.litQaProbe = String(i++);
|
|
158
|
+
el.style.cssText = `font-family:"${family}";font-weight:${weight};font-stretch:${stretch}%`;
|
|
159
|
+
el.textContent = "Aa가";
|
|
160
|
+
box.appendChild(el);
|
|
161
|
+
}
|
|
151
162
|
document.body.appendChild(box);
|
|
152
163
|
return () => box.remove();
|
|
153
164
|
},
|