@litfamily/lithermes 1.0.29 → 1.0.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -9
- package/README_Ko-KR.md +12 -9
- package/assets/lithermes-plugin/README.md +15 -2
- package/assets/lithermes-plugin/__init__.py +49 -10
- package/assets/lithermes-plugin/adhd_stack.py +228 -0
- package/assets/lithermes-plugin/core.py +30 -4
- package/assets/lithermes-plugin/core_commands.py +99 -0
- package/assets/lithermes-plugin/core_contexts.py +54 -4
- package/assets/lithermes-plugin/core_contract.py +2 -2
- package/assets/lithermes-plugin/core_routing.py +126 -0
- package/assets/lithermes-plugin/diagnostics.py +3 -2
- package/assets/lithermes-plugin/lit_mark.py +75 -0
- package/assets/lithermes-plugin/payload-version.json +470 -58
- package/assets/lithermes-plugin/plugin.yaml +1 -1
- package/assets/lithermes-plugin/scientific_visualization.py +23 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/SKILL.md +7 -7
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/fonts.json +42 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/README.md +2 -4
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/complete-contract.md +4 -4
- package/assets/lithermes-plugin/skills/frontend-ui-ux/references/craft-floor.md +10 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe-rules.json +6 -1
- package/assets/lithermes-plugin/skills/frontend-ui-ux/scripts/probe.mjs +76 -4
- package/assets/lithermes-plugin/skills/lit-adhd/SKILL.md +194 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/brief-contract.md +55 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/card-examples.md +86 -0
- package/assets/lithermes-plugin/skills/lit-adhd/references/status-taxonomy.md +40 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/adhd.mjs +11 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/collect.mjs +554 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/gauge.mjs +89 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/hermes-sessions.mjs +219 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/next-step.mjs +101 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/plan-task-rows.mjs +32 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/redact.mjs +42 -0
- package/assets/lithermes-plugin/skills/lit-adhd/scripts/lib/secret-shapes.mjs +59 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/SKILL.md +7 -4
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/style-guide.md +20 -11
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-architecture.md +3 -3
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-dependency.md +1 -1
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-tree.md +3 -3
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/type-wardley.md +1 -1
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/references/verifier-guide.md +5 -0
- package/assets/lithermes-plugin/skills/lit-diagram-drawer/scripts/diagram_quality.py +4 -4
- package/assets/lithermes-plugin/skills/lit-docx/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-hijack/SKILL.md +239 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/clean-room-build.md +128 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/assets/lithermes-plugin/skills/lit-hijack/references/parity-and-report.md +131 -0
- package/assets/lithermes-plugin/skills/lit-hijack/scripts/hijack.mjs +995 -0
- package/assets/lithermes-plugin/skills/lit-plan/SKILL.md +1 -0
- package/assets/lithermes-plugin/skills/lit-pptx/SKILL.md +11 -6
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/assets/lithermes-plugin/skills/lit-pptx/fonts/provenance.json +1 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/anti-slop-checklist.md +3 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/authoring-guide.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/consensus-protocol.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/direction-step.md +8 -2
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +175 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +156 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +141 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +160 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +183 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/layout-families.md +1 -1
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/bauhaus.md +105 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +111 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +114 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/newsroom.md +106 -0
- package/assets/lithermes-plugin/skills/lit-pptx/references/tonalities/riso.md +101 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/craft_extras.py +49 -0
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/deck_output.py +4 -2
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/render-pack.js +96 -10
- package/assets/lithermes-plugin/skills/lit-pptx/scripts/lib/template-registry.js +50 -6
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +103 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/assets/lithermes-plugin/skills/lit-recap/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/lit-schematic/SKILL.md +144 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +38 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +133 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +87 -0
- package/assets/lithermes-plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +85 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/cutaway-outputs.md +70 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/isometric-craft.md +101 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/journal-figures.md +72 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/kit-api.md +134 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pfd-and-layout.md +53 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pid-drafting.md +103 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/pid-symbols.md +163 -0
- package/assets/lithermes-plugin/skills/lit-schematic/references/verification.md +100 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/doctor.mjs +57 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/figure.mjs +81 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/fonts.mjs +27 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/geom.mjs +113 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/interactive.mjs +292 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/isa.mjs +115 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/iso.mjs +719 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +162 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/pid.mjs +937 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/png.mjs +132 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +658 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/route.mjs +218 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/sheet.mjs +890 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/svg.mjs +82 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/lib/theme.mjs +115 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/motion.mjs +129 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/qa.mjs +92 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/render.mjs +233 -0
- package/assets/lithermes-plugin/skills/lit-schematic/scripts/scaffold.mjs +60 -0
- package/assets/lithermes-plugin/skills/lit-scientific-visualization/SKILL.md +19 -1
- package/assets/lithermes-plugin/skills/lit-typographic-motion/SKILL.md +12 -11
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/motion.mjs +56 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/bin/runtime.mjs +129 -4
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +9 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/blender.mjs +154 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/board.mjs +132 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/completion.mjs +48 -5
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/gate.mjs +13 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/house.mjs +44 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/scaffold.mjs +238 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/score.mjs +643 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound-dsp.mjs +21 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/sound.mjs +171 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage-qa.mjs +23 -3
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/stage.mjs +124 -17
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/styles.mjs +240 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/treatment.mjs +152 -10
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +67 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/depth.md +74 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/studio.md +87 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/style-bibles.md +9 -2
- package/assets/lithermes-plugin/skills/lit-typographic-motion/references/treatment.md +24 -8
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/assets/lithermes-plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/assets/lithermes-plugin/skills/litresearch/SKILL.md +4 -0
- package/assets/lithermes-plugin/skills/readme-studio/SKILL.md +1 -1
- package/assets/lithermes-plugin/skills/review-work/SKILL.md +2 -0
- package/assets/lithermes-plugin/skills/start-work/SKILL.md +2 -1
- package/assets/lithermes-plugin/skills/visual-qa/references/complete-contract.md +3 -3
- package/package.json +1 -1
- package/readme-assets/badge-version.svg +1 -1
- package/src/lib/skillPayload.js +1 -1
|
@@ -270,6 +270,153 @@ export function buildBed({ out, treatment, fps, totalFrames, cuts }) {
|
|
|
270
270
|
return { file, ...cueDoc };
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
+
// ---------------------------------------------------------------------------
|
|
274
|
+
// Layered score DSP. The score engine (score.mjs) is built from these: cue
|
|
275
|
+
// planning, wavetable synthesis, the mastering stage and the WAV writer.
|
|
276
|
+
|
|
277
|
+
export const SOUND = Object.freeze({
|
|
278
|
+
sampleRate: 48000,
|
|
279
|
+
channels: 2,
|
|
280
|
+
bitsPerSample: 16,
|
|
281
|
+
targetLufs: -16,
|
|
282
|
+
lufsTolerance: 2,
|
|
283
|
+
bedPeakDbfs: -2,
|
|
284
|
+
gatePeakDbfs: -0.5,
|
|
285
|
+
silenceDbfs: -50,
|
|
286
|
+
silenceWindowSec: 0.05,
|
|
287
|
+
silenceMaxSec: 1.5,
|
|
288
|
+
silenceHeadSec: 3,
|
|
289
|
+
durationToleranceSec: 0.1,
|
|
290
|
+
fadeSec: 0.05,
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// Sounds an on-screen event can make (treatment `sound.events`): a tap (a
|
|
294
|
+
// small click with a pitched body), a hit (the cut hit), a whoosh (air ending
|
|
295
|
+
// on the event) and a chime (two notes of the key: something completes).
|
|
296
|
+
export const EVENT_SOUNDS = Object.freeze(['tap', 'hit', 'whoosh', 'chime']);
|
|
297
|
+
|
|
298
|
+
const CUE_WORDS = [
|
|
299
|
+
['hit', /\b(?:hits?|impacts?|cuts?|stabs?|accents?)\b|타격|히트|컷|강세/iu],
|
|
300
|
+
['rise', /\b(?:rises?|rising|risers?|swells?|swelling|builds?|building|build-?ups?|lifts?|lifting)\b|상승|고조|빌드업/iu],
|
|
301
|
+
['cadence', /\b(?:cadences?|resolves?|resolving|resolution|close|closing|ending|final chord)\b|종지|마무리|해결/iu],
|
|
302
|
+
];
|
|
303
|
+
|
|
304
|
+
// Cue kinds a beat's free-text `sound` field asks for; none when nothing matches.
|
|
305
|
+
export function cueKindsFor(soundText) {
|
|
306
|
+
const text = String(soundText ?? '');
|
|
307
|
+
return CUE_WORDS.filter(([, re]) => re.test(text)).map(([kind]) => kind);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const nearest = (times, t, within) => {
|
|
311
|
+
let best = null;
|
|
312
|
+
for (const c of times) if (Math.abs(c - t) <= within && (best === null || Math.abs(c - t) < Math.abs(best - t))) best = c;
|
|
313
|
+
return best;
|
|
314
|
+
};
|
|
315
|
+
const round4 = (x) => Math.round(x * 10000) / 10000;
|
|
316
|
+
|
|
317
|
+
// Accent cues, each measured against the beat boundary or cut it serves, plus
|
|
318
|
+
// one `event` cue per on-screen event at its own time (it never snaps to a
|
|
319
|
+
// cut: it follows the picture).
|
|
320
|
+
export function planCues({ beats = [], durationSec, cutTimes = [], events = [] }) {
|
|
321
|
+
const cues = [];
|
|
322
|
+
const clampT = (t) => Math.min(durationSec, Math.max(0, t));
|
|
323
|
+
beats.forEach((beat, beatIndex) => {
|
|
324
|
+
const t0 = clampT(Number(beat.t0));
|
|
325
|
+
const t1 = clampT(Number(beat.t1));
|
|
326
|
+
for (const kind of cueKindsFor(beat.sound)) {
|
|
327
|
+
if (kind === 'hit') {
|
|
328
|
+
const cut = nearest(cutTimes, t0, 0.5);
|
|
329
|
+
const target = cut ?? t0;
|
|
330
|
+
cues.push({ kind, t: round4(target), beatIndex, target: round4(target), delta: 0 });
|
|
331
|
+
} else if (kind === 'rise') {
|
|
332
|
+
const cut = nearest(cutTimes, t1, 0.5);
|
|
333
|
+
const end = cut ?? t1;
|
|
334
|
+
const start = Math.max(t0, end - 1.0);
|
|
335
|
+
cues.push({ kind, t: round4(end), start: round4(start), beatIndex, target: round4(end), delta: 0 });
|
|
336
|
+
} else {
|
|
337
|
+
const t = t0 + 0.6 * (t1 - t0);
|
|
338
|
+
cues.push({ kind, t: round4(t), beatIndex, target: round4(t), delta: 0, end: round4(beatIndex === beats.length - 1 ? durationSec : t1) });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
for (const e of Array.isArray(events) ? events : []) {
|
|
343
|
+
if (!EVENT_SOUNDS.includes(e?.kind) || !(e.t >= 0 && e.t < durationSec)) continue;
|
|
344
|
+
const beatIndex = Math.max(0, beats.findIndex((b) => e.t >= Number(b.t0) && e.t < Number(b.t1)));
|
|
345
|
+
const cut = nearest(cutTimes, e.t, Infinity);
|
|
346
|
+
cues.push({ kind: 'event', sound: e.kind, t: round4(e.t), beatIndex, target: round4(e.t), delta: 0, on: String(e.on ?? ''), fromCut: cut === null ? null : round4(Math.abs(e.t - cut)) });
|
|
347
|
+
}
|
|
348
|
+
cues.sort((a, b) => a.t - b.t || a.beatIndex - b.beatIndex);
|
|
349
|
+
return cues;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
export function samplePeakDbfs(channels) {
|
|
353
|
+
let peak = 0;
|
|
354
|
+
for (const c of channels) for (let i = 0; i < c.length; i++) {
|
|
355
|
+
const v = Math.abs(c[i]);
|
|
356
|
+
if (v > peak) peak = v;
|
|
357
|
+
}
|
|
358
|
+
return dbfs(peak);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// The shared master stage: look-ahead limiting under the ceiling, then the
|
|
362
|
+
// gain that lands the bus on the target LUFS.
|
|
363
|
+
export function masterTo(L, R) {
|
|
364
|
+
const sampleCount = L.length;
|
|
365
|
+
const ceiling = 10 ** (-2.1 / 20);
|
|
366
|
+
const ahead = Math.round(0.005 * SOUND.sampleRate);
|
|
367
|
+
const release = 1 - Math.exp(-1 / (0.08 * SOUND.sampleRate));
|
|
368
|
+
const q = (v) => Math.round(Math.max(-1, Math.min(1, v)) * 32767) / 32767;
|
|
369
|
+
const render = (gain) => {
|
|
370
|
+
const need = new Float32Array(sampleCount);
|
|
371
|
+
for (let s = 0; s < sampleCount; s++) {
|
|
372
|
+
const a = Math.max(Math.abs(L[s]), Math.abs(R[s])) * gain;
|
|
373
|
+
need[s] = a > ceiling ? ceiling / a : 1;
|
|
374
|
+
}
|
|
375
|
+
const held = new Float32Array(sampleCount);
|
|
376
|
+
const deque = new Int32Array(sampleCount + ahead + 1);
|
|
377
|
+
let head = 0;
|
|
378
|
+
let tail = 0;
|
|
379
|
+
let next = 0;
|
|
380
|
+
for (let s = 0; s < sampleCount; s++) {
|
|
381
|
+
for (; next < Math.min(sampleCount, s + ahead + 1); next++) {
|
|
382
|
+
while (tail > head && need[deque[tail - 1]] >= need[next]) tail--;
|
|
383
|
+
deque[tail++] = next;
|
|
384
|
+
}
|
|
385
|
+
while (deque[head] < s) head++;
|
|
386
|
+
held[s] = need[deque[head]];
|
|
387
|
+
}
|
|
388
|
+
let env = 1;
|
|
389
|
+
const smooth = new Float32Array(sampleCount);
|
|
390
|
+
for (let s = 0; s < sampleCount; s++) {
|
|
391
|
+
env = Math.min(held[s], env + (1 - env) * release);
|
|
392
|
+
smooth[s] = env;
|
|
393
|
+
}
|
|
394
|
+
const l = new Float32Array(sampleCount);
|
|
395
|
+
const r = new Float32Array(sampleCount);
|
|
396
|
+
let win = ahead;
|
|
397
|
+
for (let s = 0; s < sampleCount; s++) {
|
|
398
|
+
win += smooth[s] - (s >= ahead ? smooth[s - ahead] : 1);
|
|
399
|
+
const g = win / ahead;
|
|
400
|
+
l[s] = q(Math.max(-ceiling, Math.min(ceiling, L[s] * gain * g)));
|
|
401
|
+
r[s] = q(Math.max(-ceiling, Math.min(ceiling, R[s] * gain * g)));
|
|
402
|
+
}
|
|
403
|
+
return [l, r];
|
|
404
|
+
};
|
|
405
|
+
const raw = integratedLoudness([Float32Array.from(L), Float32Array.from(R)]);
|
|
406
|
+
let gain = Number.isFinite(raw) ? 10 ** ((SOUND.targetLufs - raw) / 20) : 1;
|
|
407
|
+
let [left, right] = render(gain);
|
|
408
|
+
let lufs = integratedLoudness([left, right]);
|
|
409
|
+
for (let i = 0; i < 8 && Number.isFinite(lufs) && Math.abs(lufs - SOUND.targetLufs) > 0.2; i++) {
|
|
410
|
+
gain *= 10 ** ((SOUND.targetLufs - lufs) / 20);
|
|
411
|
+
[left, right] = render(gain);
|
|
412
|
+
lufs = integratedLoudness([left, right]);
|
|
413
|
+
}
|
|
414
|
+
return { left, right, gain, lufs };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// The score writer uses the same 16-bit stereo bytes as the bed writer.
|
|
418
|
+
export const encodeWav = ({ left, right }) => wavBytes(left, right);
|
|
419
|
+
|
|
273
420
|
// ---------------------------------------------------------------------------
|
|
274
421
|
// Mux and gate.
|
|
275
422
|
|
|
@@ -338,6 +485,15 @@ export async function produceSound({ out, treatment, fps, totalFrames, cuts, vid
|
|
|
338
485
|
return { summary: { mode, label: 'no sound (silence was asked for, or the channel plays muted)' }, rules: [{ id: 'sound-stream', status: 'PASS', detail: 'no sound planned' }] };
|
|
339
486
|
}
|
|
340
487
|
let track, bed = null;
|
|
488
|
+
if (mode === 'score') {
|
|
489
|
+
const { writeScore } = await import('./score.mjs');
|
|
490
|
+
const score = writeScore({ treatment, frameCount: totalFrames, fps, cutTimes: cuts, dir: out });
|
|
491
|
+
track = score.file;
|
|
492
|
+
const label = `score, ${score.genre} in ${score.key}, ${score.tempo} BPM, ${score.stems.filter((s) => s.kind !== 'return').length} stems; ${score.progression}; ${score.lufs.toFixed(1)} LUFS, peak ${score.peakDbfs.toFixed(1)} dBFS — labelled as generated`;
|
|
493
|
+
muxAudio({ video, track, output, durationSec });
|
|
494
|
+
const gate = soundGate({ file: output, mode });
|
|
495
|
+
return { summary: { mode, label, track, score: { sha256: score.sha256, loudnessLufs: score.lufs, peakDbfs: score.peakDbfs, genre: score.genre, key: score.key, tempo: score.tempo }, mux: { peakDbfs: gate.peakDbfs, audioSec: gate.audioSec, videoSec: gate.videoSec } }, rules: gate.rules };
|
|
496
|
+
}
|
|
341
497
|
if (mode === 'generated') {
|
|
342
498
|
bed = buildBed({ out, treatment, fps, totalFrames, cuts });
|
|
343
499
|
track = bed.file;
|
|
@@ -360,6 +516,21 @@ export async function soundCommand(flags) {
|
|
|
360
516
|
const out = resolve(flags.out || 'motion-output');
|
|
361
517
|
try {
|
|
362
518
|
const { treatment } = loadTreatment(out, EXIT.BLOCKED_TREATMENT_INVALID);
|
|
519
|
+
if (treatment.sound.mode === 'score') {
|
|
520
|
+
const { writeScore } = await import('./score.mjs');
|
|
521
|
+
const fps = treatment.fps === 30 ? 30 : 60;
|
|
522
|
+
const manifestFile = join(out, 'manifest.json');
|
|
523
|
+
const manifest = existsSync(manifestFile) ? JSON.parse(readFileSync(manifestFile, 'utf8')) : null;
|
|
524
|
+
const totalFrames = manifest?.totalFrames || Math.round((manifest?.durationSec || treatment.durationSec) * fps);
|
|
525
|
+
const cuts = manifest?.path === 'stage' && manifest.beats ? manifest.beats.slice(1).map((b) => b.first / fps)
|
|
526
|
+
: manifest?.timeline ? manifest.timeline.filter((u) => u.kind === 'line').slice(1).map((u) => u.start)
|
|
527
|
+
: treatment.beats.slice(1).map((b) => b.t0);
|
|
528
|
+
const score = writeScore({ treatment, frameCount: totalFrames, fps: manifest?.fps || fps, cutTimes: cuts, dir: out });
|
|
529
|
+
const sections = score.sections.map((s) => s.kind).join(' → ');
|
|
530
|
+
console.log(`sound: sound/score.wav (score, ${score.genre} in ${score.key}, ${score.tempo} BPM; ${score.progression}; ${sections}; sidechain ${score.sidechainDb.toFixed(1)} dB; ${score.lufs.toFixed(1)} LUFS, peak ${score.peakDbfs.toFixed(1)} dBFS; stems in sound/stems)`);
|
|
531
|
+
console.log(`cues: ${join(out, 'sound-cues.json')} (${score.cues.length} cues)`);
|
|
532
|
+
return 0;
|
|
533
|
+
}
|
|
363
534
|
if (treatment.sound.mode !== 'generated') {
|
|
364
535
|
console.log(`sound: the treatment's sound mode is ${treatment.sound.mode}; nothing to generate`);
|
|
365
536
|
return 0;
|
|
@@ -131,6 +131,15 @@ const PAGE_QA = String(function install() {
|
|
|
131
131
|
};
|
|
132
132
|
});
|
|
133
133
|
|
|
134
|
+
export function copyOverlaps(boxes) {
|
|
135
|
+
const hits = [];
|
|
136
|
+
for (let i = 0; i < boxes.length; i++) for (let j = i + 1; j < boxes.length; j++) {
|
|
137
|
+
const [ax0, ay0, ax1, ay1] = boxes[i], [bx0, by0, bx1, by1] = boxes[j];
|
|
138
|
+
if (Math.min(ax1, bx1) > Math.max(ax0, bx0) && Math.min(ay1, by1) > Math.max(ay0, by0)) hits.push([i, j]);
|
|
139
|
+
}
|
|
140
|
+
return hits;
|
|
141
|
+
}
|
|
142
|
+
|
|
134
143
|
function inkMask(a, b, width, height, rects) {
|
|
135
144
|
const mask = Buffer.alloc(width * height);
|
|
136
145
|
const inside = new Uint8Array(width * height);
|
|
@@ -191,7 +200,7 @@ export async function runTextQa(session) {
|
|
|
191
200
|
const tenFps = Math.max(1, Math.round(fps / 10));
|
|
192
201
|
const visibleSpans = new Map();
|
|
193
202
|
const seenCopy = new Set();
|
|
194
|
-
const results = { contrast: [], safe: [], decorCopy: [], decorArea: [], meta: new Set(), fonts: [], moved: [], canvasUnmeasured: false, nonText: [], floors: [] };
|
|
203
|
+
const results = { contrast: [], safe: [], decorCopy: [], decorArea: [], meta: new Set(), fonts: [], moved: [], canvasUnmeasured: false, nonText: [], floors: [], overlap: [] };
|
|
195
204
|
const stage = await session.open('q');
|
|
196
205
|
const cdp = stage.cdp;
|
|
197
206
|
const PNG = session.PNG;
|
|
@@ -215,7 +224,9 @@ export async function runTextQa(session) {
|
|
|
215
224
|
const now = new Map();
|
|
216
225
|
for (const run of runs) {
|
|
217
226
|
const visible = run.opacity >= 0.6 && run.rects.length > 0;
|
|
218
|
-
|
|
227
|
+
// The floor span is the copy line's, not its position bucket's: a line
|
|
228
|
+
// that slides (a push handoff moves its box) keeps one continuous span.
|
|
229
|
+
const key = run.norm;
|
|
219
230
|
now.set(run.id, run);
|
|
220
231
|
if (!visible || !run.norm) continue;
|
|
221
232
|
if (isCopy(run.norm)) for (const c of copyLines) if (c.norm.includes(run.norm) || run.norm.includes(c.norm)) seenCopy.add(c.line);
|
|
@@ -268,6 +279,12 @@ export async function runTextQa(session) {
|
|
|
268
279
|
}
|
|
269
280
|
if (textArea > 0 && decorArea / textArea > 0.25) results.decorArea.push(`${Math.round((100 * decorArea) / textArea)} % at frame ${f}`);
|
|
270
281
|
if (sample.kind === 'mid') results.nonText.push(lumStdOutside(b, W, H, visibleRuns.flatMap((r) => r.rects)));
|
|
282
|
+
if (sample.kind === 'settled') {
|
|
283
|
+
const settledCopy = visibleRuns.filter((r) => !r.decor && r.opacity >= 0.95 && isCopy(r.norm));
|
|
284
|
+
for (const [i, j] of copyOverlaps(settledCopy.map((r) => r.box))) {
|
|
285
|
+
results.overlap.push(`"${settledCopy[i].text.slice(0, 30)}" over "${settledCopy[j].text.slice(0, 30)}" at frame ${f}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
271
288
|
}
|
|
272
289
|
prevRuns = now;
|
|
273
290
|
}
|
|
@@ -287,7 +304,7 @@ export async function runTextQa(session) {
|
|
|
287
304
|
if (missing.length) throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: copy line not on screen: "${missing[0].line}"${missing.length > 1 ? ` (+${missing.length - 1} more)` : ''}; every copy.lines entry must appear as text the QA can read`);
|
|
288
305
|
|
|
289
306
|
const rules = [];
|
|
290
|
-
const row = (id, status, detail) => rules.push({ id, status, detail });
|
|
307
|
+
const row = (id, status, detail) => { rules.push({ id, status, detail }); if (!QA_ROW_IDS.includes(id)) QA_ROW_IDS.push(id); };
|
|
291
308
|
const lowCopy = results.contrast.filter((c) => !c.decor && c.ratio < c.floor);
|
|
292
309
|
const lowDecor = results.contrast.filter((c) => c.decor && c.ratio < c.floor);
|
|
293
310
|
const worst = results.contrast.reduce((m, c) => (m && m.ratio / m.floor <= c.ratio / c.floor ? m : c), null);
|
|
@@ -307,9 +324,12 @@ export async function runTextQa(session) {
|
|
|
307
324
|
row('qa-samples', results.moved.length ? 'WARN' : 'PASS', results.moved.length ? `state moved while the text was hidden at frames ${results.moved.slice(0, 6).join(', ')}; those samples were discarded` : `${samples.size} samples measured`);
|
|
308
325
|
const lively = results.nonText.filter((s) => s > 8 / 255).length;
|
|
309
326
|
row('non-text-presence', results.nonText.length && lively * 2 >= results.nonText.length ? 'PASS' : 'WARN', `${lively} of ${results.nonText.length} beat midpoints show a non-text image (luminance spread over 8/255 outside the text)`);
|
|
327
|
+
row('text-overlap', results.overlap.length ? 'FAIL' : 'PASS', results.overlap.length ? `settled copy overlaps: ${results.overlap[0]}` : 'no two settled copy lines overlap');
|
|
310
328
|
return { rules, summary: { samples: [...samples.keys()].sort((a, b) => a - b), contrast: results.contrast.length, copySeen: [...seenCopy], meta: [...results.meta] } };
|
|
311
329
|
}
|
|
312
330
|
|
|
331
|
+
export const QA_ROW_IDS = ['text-contrast', 'text-title-safe', 'text-reading-floor', 'copy-found', 'decor-copy', 'decor-area', 'meta-label', 'fonts', 'canvas-text', 'qa-samples', 'non-text-presence', 'text-overlap'];
|
|
332
|
+
|
|
313
333
|
// The platform names of this product's verified faces, read from probe spans.
|
|
314
334
|
async function faceNames(stage) {
|
|
315
335
|
await stage.page.evaluate(() => {
|
|
@@ -8,20 +8,39 @@ import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSy
|
|
|
8
8
|
import { dirname, extname, join, relative, resolve, sep } from 'node:path';
|
|
9
9
|
import { fileURLToPath } from 'node:url';
|
|
10
10
|
import { Worker } from 'node:worker_threads';
|
|
11
|
-
import { ALWAYS_FLAGS, EXIT, OUTPUT } from './constants.mjs';
|
|
11
|
+
import { ALWAYS_FLAGS, EXIT, OUTPUT, chromeFlagLadder } from './constants.mjs';
|
|
12
12
|
import { chromeExecutable, launchCause, profileSockets } from './browser.mjs';
|
|
13
13
|
import { FlashDetector } from './flash.mjs';
|
|
14
14
|
import { encodePreview, ffprobe, hasFfmpeg, pngEncode, startMaster } from './encode.mjs';
|
|
15
15
|
import { FORMATS, loadTreatment } from './treatment.mjs';
|
|
16
|
+
import { readPlate } from './blender.mjs';
|
|
16
17
|
import { recordFirstTreatment, writeJson, writeRunState } from './director.mjs';
|
|
17
18
|
import { stampStills, stillsPlan, writeStillsSet } from './stills.mjs';
|
|
18
19
|
import { runStageGate, renderStageReport } from './stage-gate.mjs';
|
|
19
20
|
import { produceSound, soundGate } from './sound.mjs';
|
|
20
21
|
import { runTextQa } from './stage-qa.mjs';
|
|
22
|
+
import { readStyle, styleCss, styleScript } from './styles.mjs';
|
|
23
|
+
import { addToStills, writeBoard } from './board.mjs';
|
|
21
24
|
import { FONT_FILES } from './frame.mjs';
|
|
25
|
+
|
|
22
26
|
import { findCache, fontFile, fontState, hangulPath, runtimeRequire, WARM } from '../bin/runtime.mjs';
|
|
23
27
|
|
|
24
28
|
const here = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
// The pinned three.js build and its two addons (engine/vendor/three, MIT),
|
|
30
|
+
// served to stage pages at /lit/three/; anything else under that prefix is refused.
|
|
31
|
+
const THREE_DIR = join(here, 'vendor', 'three');
|
|
32
|
+
|
|
33
|
+
// Resolve a /lit/three/ request to a vendored file: only .js paths without ..
|
|
34
|
+
// that exist under engine/vendor/three. Anything else is refused, never served.
|
|
35
|
+
export function threeFile(pathname) {
|
|
36
|
+
const rel = String(pathname).slice('/lit/three/'.length);
|
|
37
|
+
if (rel.split('/').includes('..') || !rel.endsWith('.js')) return { status: 404, reason: `request for ${pathname} is not a vendored three.js file` };
|
|
38
|
+
let real = null;
|
|
39
|
+
try { real = realpathSync(join(THREE_DIR, rel)); } catch { return { status: 404, reason: `request for ${pathname} is not a vendored three.js file` }; }
|
|
40
|
+
const root = realpathSync(THREE_DIR);
|
|
41
|
+
if (real !== root && !real.startsWith(root + sep)) return { status: 404, reason: `request for ${pathname} is not a vendored three.js file` };
|
|
42
|
+
return { status: 200, file: real };
|
|
43
|
+
}
|
|
25
44
|
export const ORIGIN = 'http://lit.stage';
|
|
26
45
|
const sha = (data) => createHash('sha256').update(data).digest('hex');
|
|
27
46
|
const fail = (code, message, extra = {}) => Object.assign(new Error(message), { exitCode: code, ...extra });
|
|
@@ -39,6 +58,19 @@ export const RASTER_LIMITS = Object.freeze({ count: 24, bytes: 8 * 1024 * 1024,
|
|
|
39
58
|
// The software rung only (SwiftShader, CPU raster): a mismatch between two
|
|
40
59
|
// captures can then only come from the page. Plus the stage set and the two
|
|
41
60
|
// keychain flags every Chrome launch carries.
|
|
61
|
+
// The 3D route's flag set: the platform's GPU rung first, then the software
|
|
62
|
+
// rung; the determinism flags after the rung are the same as the software rung's.
|
|
63
|
+
export function gpuStageFlags({ width, height }, platform = process.platform) {
|
|
64
|
+
return [...chromeFlagLadder(platform)[0], ...stageFlags({ width, height }).slice(3)];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// The browser rung: a three.js world asks for the GPU; anything else (a 2D
|
|
68
|
+
// film, or a Blender film that shows its path-traced plate under HTML type)
|
|
69
|
+
// needs only the software rung.
|
|
70
|
+
export function stageRung(treatment) {
|
|
71
|
+
return treatment?.depth === '3d' && treatment?.render3d !== 'blender' ? 'gpu' : 'software';
|
|
72
|
+
}
|
|
73
|
+
|
|
42
74
|
export function stageFlags({ width, height }) {
|
|
43
75
|
return [
|
|
44
76
|
'--use-angle=swiftshader', '--enable-unsafe-swiftshader', '--disable-gpu-rasterization',
|
|
@@ -221,25 +253,62 @@ export function stageFile(root, pathname) {
|
|
|
221
253
|
return { status: 200, file: real, type };
|
|
222
254
|
}
|
|
223
255
|
|
|
224
|
-
|
|
256
|
+
const webglProbe = () => {
|
|
257
|
+
const canvas = document.createElement('canvas');
|
|
258
|
+
const gl = canvas.getContext('webgl2');
|
|
259
|
+
if (!gl) return { webgl2: false, renderer: null };
|
|
260
|
+
const ext = gl.getExtension('WEBGL_debug_renderer_info');
|
|
261
|
+
return { webgl2: true, renderer: ext ? gl.getParameter(ext.UNMASKED_RENDERER_WEBGL) : null };
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export async function openStage({ runtime, stageDir, profileRoot, width, height, seed, fontCss, fontFiles, style = null, rung = 'software', platform = process.platform, env = process.env }) {
|
|
225
265
|
const chrome = chromeExecutable(env);
|
|
226
266
|
if (!chrome) throw fail(EXIT.BLOCKED_NO_CHROME, 'BLOCKED_NO_CHROME: Chrome/Chromium not found; install Chrome or set CHROME_PATH');
|
|
227
267
|
const { chromium } = runtime('playwright-core');
|
|
228
|
-
const flags = stageFlags({ width, height });
|
|
229
268
|
mkdirSync(profileRoot, { recursive: true });
|
|
230
269
|
const profile = join(profileRoot, 'p');
|
|
270
|
+
const launch = (flags) => chromium.launchPersistentContext(profile, {
|
|
271
|
+
executablePath: chrome, headless: true, chromiumSandbox: true, args: flags, timeout: 30000,
|
|
272
|
+
viewport: { width, height }, deviceScaleFactor: 1, serviceWorkers: 'block',
|
|
273
|
+
});
|
|
274
|
+
const notices = [];
|
|
275
|
+
let flags = rung === 'gpu' ? gpuStageFlags({ width, height }, platform) : stageFlags({ width, height });
|
|
276
|
+
let used = rung;
|
|
231
277
|
let context;
|
|
232
278
|
try {
|
|
233
|
-
context = await
|
|
234
|
-
executablePath: chrome, headless: true, chromiumSandbox: true, args: flags, timeout: 30000,
|
|
235
|
-
viewport: { width, height }, deviceScaleFactor: 1, serviceWorkers: 'block',
|
|
236
|
-
});
|
|
279
|
+
context = await launch(flags);
|
|
237
280
|
} catch (error) {
|
|
238
|
-
|
|
239
|
-
|
|
281
|
+
if (rung !== 'gpu') {
|
|
282
|
+
rmSync(profile, { recursive: true, force: true });
|
|
283
|
+
throw fail(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: headless Chrome failed to launch on the software rung: ${launchCause(error)}`);
|
|
284
|
+
}
|
|
285
|
+
notices.push(`the 3D route asked for the GPU rung and it did not start (${launchCause(error).slice(0, 120)}); rendered on the software rung (SwiftShader), slower, same frames`);
|
|
286
|
+
flags = stageFlags({ width, height });
|
|
287
|
+
used = 'software';
|
|
288
|
+
try {
|
|
289
|
+
context = await launch(flags);
|
|
290
|
+
} catch (retry) {
|
|
291
|
+
rmSync(profile, { recursive: true, force: true });
|
|
292
|
+
throw fail(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: headless Chrome failed to launch on both rungs: ${launchCause(retry)}`);
|
|
293
|
+
}
|
|
240
294
|
}
|
|
241
|
-
|
|
295
|
+
let page = context.pages()[0] || await context.newPage();
|
|
242
296
|
page.setDefaultTimeout(0);
|
|
297
|
+
const probe = await page.evaluate(webglProbe).catch(() => ({ webgl2: false, renderer: null }));
|
|
298
|
+
if (used === 'gpu' && !probe.webgl2) {
|
|
299
|
+
notices.push('the 3D route asked for the GPU rung and the page got no WebGL there; rendered on the software rung (SwiftShader), slower, same frames');
|
|
300
|
+
await context.close().catch(() => {});
|
|
301
|
+
flags = stageFlags({ width, height });
|
|
302
|
+
used = 'software';
|
|
303
|
+
try {
|
|
304
|
+
context = await launch(flags);
|
|
305
|
+
} catch (error) {
|
|
306
|
+
rmSync(profile, { recursive: true, force: true });
|
|
307
|
+
throw fail(EXIT.BLOCKED_NO_CHROME, `BLOCKED_NO_CHROME: headless Chrome failed to launch on the software rung: ${launchCause(error)}`);
|
|
308
|
+
}
|
|
309
|
+
page = context.pages()[0] || await context.newPage();
|
|
310
|
+
page.setDefaultTimeout(0);
|
|
311
|
+
}
|
|
243
312
|
const cdp = await context.newCDPSession(page);
|
|
244
313
|
const root = realpathSync(stageDir);
|
|
245
314
|
const kit = readFileSync(join(here, 'stage-kit.js'));
|
|
@@ -260,6 +329,16 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
260
329
|
const path = decodeURIComponent(url.pathname);
|
|
261
330
|
if (path === '/lit/stage-kit.js') return reply(200, CONTENT_TYPES['.js'], kit);
|
|
262
331
|
if (path === '/lit/fonts.css') return reply(200, CONTENT_TYPES['.css'], fontCss);
|
|
332
|
+
if (path === '/lit/style.css' && style) return reply(200, CONTENT_TYPES['.css'], style.css);
|
|
333
|
+
if (path === '/lit/style.js' && style) return reply(200, CONTENT_TYPES['.js'], style.js);
|
|
334
|
+
if (path.startsWith('/lit/three/')) {
|
|
335
|
+
const found = threeFile(path);
|
|
336
|
+
if (found.status !== 200) {
|
|
337
|
+
contract.push(found.reason);
|
|
338
|
+
return reply(404, 'text/plain', 'not found');
|
|
339
|
+
}
|
|
340
|
+
return reply(200, CONTENT_TYPES['.js'], readFileSync(found.file));
|
|
341
|
+
}
|
|
263
342
|
if (fontFiles.has(path)) return reply(200, CONTENT_TYPES[extname(path).toLowerCase()] || 'font/ttf', readFileSync(fontFiles.get(path)));
|
|
264
343
|
const found = stageFile(root, path);
|
|
265
344
|
if (found.status === 403) { contract.push(found.reason); return reply(403, 'text/plain', 'forbidden'); }
|
|
@@ -272,7 +351,7 @@ export async function openStage({ runtime, stageDir, profileRoot, width, height,
|
|
|
272
351
|
await page.addInitScript({ content: `try { ${init.trim()}(${JSON.stringify({ seed, epochMs: 1767225600000 })}); } catch (error) { window.__litStageInitError = String(error && error.stack || error); }` });
|
|
273
352
|
const sockets = profileSockets(profile);
|
|
274
353
|
return {
|
|
275
|
-
page, cdp, flags, blocked, contract, sockets,
|
|
354
|
+
page, cdp, flags, blocked, contract, sockets, rung: used, renderer: probe.renderer, notices,
|
|
276
355
|
async load() {
|
|
277
356
|
await page.goto(`${ORIGIN}/index.html`, { waitUntil: 'load', timeout: 120000 });
|
|
278
357
|
return page.evaluate(() => {
|
|
@@ -437,12 +516,22 @@ class StageSession {
|
|
|
437
516
|
this.PNG = this.require('pngjs').PNG;
|
|
438
517
|
this.fonts = fontRoutes(this.cache);
|
|
439
518
|
this.ffmpeg = hasFfmpeg();
|
|
519
|
+
const filmStyle = readStyle(this.treatment.style);
|
|
520
|
+
this.style = { css: styleCss(filmStyle, this.treatment.format), js: styleScript(filmStyle, this.treatment.format, this.treatment) };
|
|
521
|
+
this.rung = stageRung(this.treatment);
|
|
522
|
+
// A Blender film shows its path-traced plate under HTML type, so it shares the page's
|
|
523
|
+
// run with a 3-D-of-any-kind page: render the plate first, blender --out DIR --final
|
|
524
|
+
// (no Blender: set render3d to three and use the GPU rung).
|
|
525
|
+
if (this.treatment.depth === '3d' && this.treatment.render3d === 'blender' && !readPlate(this.stageDir)) {
|
|
526
|
+
throw fail(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: render3d is blender but stage/plate/plate.json is missing or does not match its frames; render the plate first: blender --out ${this.out} --final (no Blender: set render3d to three and use the GPU rung)`);
|
|
527
|
+
}
|
|
440
528
|
}
|
|
441
529
|
|
|
442
|
-
open(tag) {
|
|
530
|
+
open(tag, { stageDir = this.stageDir, width = this.size[0], height = this.size[1] } = {}) {
|
|
443
531
|
return openStage({
|
|
444
|
-
runtime: this.require, stageDir
|
|
445
|
-
width
|
|
532
|
+
runtime: this.require, stageDir, profileRoot: join(this.out, '.run', `${tag}${process.pid % 100000}`),
|
|
533
|
+
width, height, seed: this.seed, fontCss: this.fonts.css, fontFiles: this.fonts.routes, style: this.style,
|
|
534
|
+
rung: this.rung || 'software',
|
|
446
535
|
});
|
|
447
536
|
}
|
|
448
537
|
|
|
@@ -576,7 +665,10 @@ class StageSession {
|
|
|
576
665
|
schemaVersion: 2, path: 'stage', format: this.treatment.format, fps: this.fps, resolution: [W, H], durationSec: this.durationSec,
|
|
577
666
|
targetDurationSec: this.treatment.durationSec, totalFrames: this.totalFrames, round: this.round, mode: this.mode,
|
|
578
667
|
treatment: { file: join(this.out, 'treatment.json'), sha256: sha(readFileSync(join(this.out, 'treatment.json'))) },
|
|
579
|
-
chromeFlags: this.stage?.flags || [], chromeSockets: this.stage?.sockets || [],
|
|
668
|
+
chromeFlags: this.stage?.flags || [], chromeSockets: this.stage?.sockets || [],
|
|
669
|
+
rung: this.stage?.rung || this.rung || 'software',
|
|
670
|
+
renderer: this.stage?.rung === 'gpu' ? (this.stage?.renderer || 'GPU rung') : 'SwiftShader (software rung)',
|
|
671
|
+
rungNotices: this.stage?.notices || [],
|
|
580
672
|
faces: this.faces, scan: this.scan, beats: this.beats, stills: this.plan, determinism: this.determinismResult || null,
|
|
581
673
|
frameTimeMs: { p50: pick(0.5), p95: pick(0.95), captured: timing.length }, masterSec: this.masterSec, warnings: this.warnings,
|
|
582
674
|
generatedAt: new Date().toISOString(), ...extra,
|
|
@@ -597,9 +689,24 @@ export async function stageCommand(flags) {
|
|
|
597
689
|
writeJson(join(session.out, 'manifest.json'), manifest);
|
|
598
690
|
stampStills(session.out);
|
|
599
691
|
if (!session.ffmpeg && !session.stillsOnly) throw fail(EXIT.BLOCKED_NO_FFMPEG_FOR_VIDEO, `BLOCKED_NO_FFMPEG_FOR_VIDEO: stills and the contact sheet are in ${session.out}; ffmpeg is not on PATH, so no film was encoded`);
|
|
692
|
+
// The board round: the text and style rules run on the board frames, the storyboard is
|
|
693
|
+
// drawn with the verdict on it, and a FAIL ends the round here (exit 13).
|
|
694
|
+
const qa = await runTextQa(session);
|
|
695
|
+
writeJson(join(session.out, 'stills', 'check.json'), { rules: qa.rules, summary: qa.summary });
|
|
696
|
+
const beatFiles = stills.files.filter((f) => f.kind === 'beat-mid').sort((a, b) => a.beat - b.beat).map((f) => join(session.out, f.file));
|
|
697
|
+
const boardFile = await writeBoard({
|
|
698
|
+
out: session.out, treatment: session.treatment, beatFiles, check: { rules: qa.rules },
|
|
699
|
+
open: (opts) => session.open('b', opts),
|
|
700
|
+
});
|
|
701
|
+
const boardSha = addToStills(session.out, { file: 'stills/board.png', kind: 'board', frames: session.beats.map((b) => b.index) }, { sha256: sha });
|
|
702
|
+
const failed = qa.rules.filter((rule) => rule.status === 'FAIL');
|
|
703
|
+
if (failed.length) {
|
|
704
|
+
writeRunState(session.out, { exitCode: EXIT.GATE_FAIL_QA, mode: session.mode, round: session.round, finished: false, failed: failed.map((r) => r.id), path: 'stage' });
|
|
705
|
+
throw fail(EXIT.GATE_FAIL_QA, `BOARD_CHECK_FAIL: ${failed.map((r) => `${r.id} ${r.detail}`).join('; ')}. Fix the page, render --stills-only again, then look at the board ${boardFile} and every file in ${join(session.out, 'stills')}`);
|
|
706
|
+
}
|
|
600
707
|
writeRunState(session.out, { exitCode: 0, mode: session.mode, round: session.round, finished: true, failed: [], path: 'stage' });
|
|
601
|
-
console.log(`stills: ${stills.files.map((f) => join(session.out, f.file)).join(' ')}`);
|
|
602
|
-
console.log(
|
|
708
|
+
console.log(`stills: ${stills.files.map((f) => join(session.out, f.file)).join(' ')} board ${boardFile} (stills index ${boardSha.slice(0, 12)})`);
|
|
709
|
+
console.log(`STILLS ONLY: board check PASS; open the storyboard ${boardFile} and every file in ${join(session.out, 'stills')}, record the round with look --round ${session.round}, make the change, then render the film with --round ${session.round + 1} (no --stills-only).`);
|
|
603
710
|
return 0;
|
|
604
711
|
}
|
|
605
712
|
await session.determinism();
|