@litfamily/litmuse 0.1.1 → 0.1.3
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 +21 -0
- package/README.md +23 -1
- package/README_ko-KR.md +23 -1
- package/TRADEMARK.md +12 -0
- package/bin/litmuse.mjs +4 -0
- package/lib/stage-card.mjs +249 -6
- package/package.json +3 -2
- package/plugin/.muse-plugin/plugin.json +26 -11
- package/plugin/commands/lit-status.md +3 -1
- package/plugin/commands/lit.md +1 -1
- package/plugin/hooks/lib/digest.mjs +1 -1
- package/plugin/hooks/lib/next-step.mjs +112 -0
- package/plugin/hooks/lib/routes.mjs +59 -0
- package/plugin/hooks/lib/stage.mjs +53 -6
- package/plugin/hooks/lib/workflow-stack.mjs +336 -0
- package/plugin/hooks/post-tool-use-failure.mjs +2 -0
- package/plugin/hooks/post-tool-use.mjs +2 -0
- package/plugin/hooks/session-end.mjs +2 -1
- package/plugin/hooks/session-start.mjs +32 -2
- package/plugin/hooks/stop.mjs +23 -3
- package/plugin/hooks/user-prompt-submit.mjs +138 -7
- package/plugin/lib/capped-fetch.mjs +25 -0
- package/plugin/lib/pinned-fetch.mjs +79 -0
- package/plugin/lib/plan-task-rows.mjs +32 -0
- package/plugin/lib/render_pages.py +29 -1
- package/plugin/lib/rewind/collect.mjs +808 -0
- package/plugin/lib/rewind/gauge.mjs +72 -0
- package/plugin/lib/rewind/next-step.mjs +91 -0
- package/plugin/lib/rewind/redact.mjs +83 -0
- package/plugin/lib/rewind/transcripts.mjs +229 -0
- package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
- package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
- package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
- package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
- package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
- package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
- package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/plugin/skills/lit-adhd/SKILL.md +195 -0
- package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +45 -0
- package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
- package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
- package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
- package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
- package/plugin/skills/lit-docx/SKILL.md +1 -1
- package/plugin/skills/lit-hijack/SKILL.md +589 -0
- package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
- package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
- package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
- package/plugin/skills/lit-plan/SKILL.md +12 -1
- package/plugin/skills/lit-plan/scripts/check-plan.mjs +71 -4
- package/plugin/skills/lit-pptx/SKILL.md +20 -9
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
- package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
- package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
- package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
- package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
- package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
- package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
- package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
- package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
- package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
- package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/plugin/skills/lit-recap/SKILL.md +3 -0
- package/plugin/skills/lit-schematic/SKILL.md +241 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
- package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
- package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
- package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
- package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
- package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
- package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
- package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
- package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
- package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
- package/plugin/skills/lit-schematic/references/verification.md +24 -0
- package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
- package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
- package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
- package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
- package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
- package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
- package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
- package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
- package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
- package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
- package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
- package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
- package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
- package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/plugin/skills/lit-team/scripts/state-root.mjs +5 -18
- package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
- package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
- package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
- package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
- package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
- package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
- package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
- package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
- package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
- package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
- package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
- package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
- package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
- package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
- package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
- package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
- package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
- package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
- package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
- package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
- package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
- package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
- package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
- package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
- package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/plugin/skills/readme-studio/SKILL.md +1 -1
- package/plugin/skills/start-work/SKILL.md +10 -1
|
@@ -5,21 +5,45 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Normalization for every comparison: NFC, lowercase, then no whitespace, punctuation or symbols.
|
|
7
7
|
// Quoted spans are removed from the request before an `idea` or invented copy is compared with it.
|
|
8
|
+
//
|
|
9
|
+
// The forks that used to be the model's to flip on every run are rules here: the house style (named,
|
|
10
|
+
// auto-pick), the format (9:16 only for a vertical request), the length (the genre's band unless the
|
|
11
|
+
// user names one) and the genre (a genre word in the request fixes it). Concept fields stay free.
|
|
12
|
+
import { createHash } from "node:crypto";
|
|
8
13
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
14
|
import path from "node:path";
|
|
10
15
|
import { fileURLToPath } from "node:url";
|
|
11
|
-
import {
|
|
16
|
+
import { EXIT } from "../core/constants.mjs";
|
|
17
|
+
import { BlockedError } from "./chrome.mjs";
|
|
18
|
+
import { readHouse } from "./house.mjs";
|
|
19
|
+
import { LAYOUT_IDS, STYLE_IDS, styleBeds, styleColors, styleFamilies, pickStyle, readStyle } from "./styles.mjs";
|
|
20
|
+
import { SCORE_GENRES } from "./score.mjs";
|
|
21
|
+
import { EVENT_SOUNDS, normalizePalette, SoundError } from "./sound.mjs";
|
|
12
22
|
|
|
13
23
|
export const GENRES = Object.freeze(["announcement", "brand-mood", "event", "explainer", "motion-graphics", "type-led", "other"]);
|
|
14
24
|
/** Minimum beats per genre arc; `type-led` needs one per supplied line instead. */
|
|
15
25
|
export const ARC_STAGES = Object.freeze({ announcement: 5, "brand-mood": 4, event: 4, explainer: 4, "motion-graphics": 5, other: 3 });
|
|
16
26
|
export const TEN_SECOND_FLOOR = Object.freeze(["announcement", "event", "explainer", "motion-graphics"]);
|
|
17
|
-
|
|
27
|
+
/** Length bands in seconds when the user names no length; `type-led` scales with its lines. */
|
|
28
|
+
export const GENRE_BANDS = Object.freeze({ announcement: [12, 18], "brand-mood": [10, 15], event: [10, 15], explainer: [16, 24], "motion-graphics": [12, 18], other: [10, 15] });
|
|
29
|
+
export const typeLedBand = (lines) => [Math.max(4, 2 * lines), Math.max(8, 6 * lines)];
|
|
30
|
+
export const DEVICE_KINDS = Object.freeze(["illustration", "diagram", "chart", "icon", "shape", "path", "mask", "depth3d", "screen", "particles", "grid", "gradient", "photo-texture"]);
|
|
18
31
|
export const TEXTURE_KINDS = Object.freeze(["grid", "gradient", "particles", "photo-texture"]);
|
|
19
32
|
export const FACES = Object.freeze(["Pretendard", "Archivo", "VT323", "Silkscreen", "Galmuri9", "MesloLGS NF"]);
|
|
20
33
|
export const FORMATS = Object.freeze({ "16:9": [1920, 1080], "9:16": [1080, 1920] });
|
|
21
34
|
export const MIN_BEAT_SEC = 1.2;
|
|
22
35
|
export const MAX_GAP_SEC = 0.25;
|
|
36
|
+
export const PROTAGONIST_KINDS = Object.freeze(["character", "object"]);
|
|
37
|
+
/** What a product asset is; `screen` devices need one of the last three. */
|
|
38
|
+
export const ASSET_ROLES = Object.freeze(["logo", "wordmark", "icon", "photo", "screenshot", "ui", "readme-image"]);
|
|
39
|
+
const SCREEN_ROLES = new Set(["screenshot", "ui", "readme-image"]);
|
|
40
|
+
/** An event hit counts as off the cut when it is at least this far from every cut. */
|
|
41
|
+
export const EVENT_OFF_CUT_SEC = 0.1;
|
|
42
|
+
/** How dense a film is. Rich is the default; minimal only when the request asks for calm or minimal. */
|
|
43
|
+
export const REGISTERS = Object.freeze(["rich", "minimal"]);
|
|
44
|
+
/** What a rich stage film's world plans at least: depth layers, labelled parts, a share of moving shots. */
|
|
45
|
+
export const RICH_WORLD = Object.freeze({ layers: 3, labels: 6, labelWords: 4, cameraShare: 0.5 });
|
|
46
|
+
const LOCKED_CAMERA = /^\s*(?:none|locked|static|still|hold|fixed|no move)\s*\.?\s*$/iu;
|
|
23
47
|
|
|
24
48
|
const TREATMENT_MD = fileURLToPath(new URL("../../references/treatment.md", import.meta.url));
|
|
25
49
|
|
|
@@ -58,6 +82,53 @@ export function typeLedCue(request) {
|
|
|
58
82
|
const SILENCE_ASKED = /무음|소리\s?없|음악\s?없|소리\s?끄|\bsilent\b|\bsilence\b|\bno\s+(?:sound|music|audio)\b|\bwithout\s+(?:sound|music|audio)\b|\bmuted?\b/iu;
|
|
59
83
|
const MUTED_CHANNEL = /\bmuted\b|\bmute\b|\bsilent\b|\bno\s+sound\b|무음|소리\s?없이|음소거/iu;
|
|
60
84
|
const LENGTH_ASKED = /\d+(?:\.\d+)?\s*(?:초|분|s\b|sec\b|secs\b|seconds?\b|min\b|mins\b|minutes?\b)/iu;
|
|
85
|
+
const LENGTH_VALUE = /(\d+(?:\.\d+)?)\s*(초|분|s\b|sec\b|secs\b|seconds?\b|min\b|mins\b|minutes?\b)/iu;
|
|
86
|
+
const VERTICAL = /세로|숏폼|쇼츠|릴스|틱톡|인스타(?:그램)?\s?스토리|\bvertical\b|\bportrait\s+(?:format|mode|video|orientation)\b|9\s?:\s?16|\bshorts\b|\breels?\b|\btiktok\b|\binsta(?:gram)?\s+stor(?:y|ies)\b/iu;
|
|
87
|
+
const MINIMAL_ASKED = /미니멀|잔잔|차분|조용|심플|절제|단순하게|고요|\bminimal(?:ist|ism)?\b|\bcalm\b|\bquiet\b|\bsimple\b|\bunderstated\b|\brestrained\b|\bsparse\b|\bserene\b/iu;
|
|
88
|
+
const PREVIEW_ASKED = /미리\s?보기|스토리보드|콘티|\bpreview\b|\bstoryboard\b/iu;
|
|
89
|
+
const FPS30_ASKED = /30\s?(?:fps|프레임|frames?\s+(?:a|per)\s+second)|\b30\s?p\b/iu;
|
|
90
|
+
// A link in the request names a real product, repository or site: the film uses its real assets.
|
|
91
|
+
const URL_IN_REQUEST = /\bhttps?:\/\/[^\s"'<>)\]]+/giu;
|
|
92
|
+
// A genre word in the request fixes the genre; checked in order, first hit wins.
|
|
93
|
+
const GENRE_CUES = Object.freeze([
|
|
94
|
+
["explainer", /설명|원리|\bexplain(?:er|s|ing)?\b/iu],
|
|
95
|
+
["announcement", /출시|런칭|론칭|티저|신제품|\blaunch(?:es|ing)?\b|\bteaser\b|\bannounce(?:ment|s)?\b|\bintroducing\b/iu],
|
|
96
|
+
["event", /행사|공연|축제|모집|세미나|콘서트|음악회|전시회|\bevent\b|\bfestival\b|\bconcert\b|\bmeetup\b|\bconference\b|\bexhibition\b/iu],
|
|
97
|
+
["explainer", /어떻게|과정|\bhow\s+(?:it|they|a|an|the|to|\w+\s+works?)\b/iu],
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
/** The length the request asks for, in seconds, or null. */
|
|
101
|
+
export function askedSeconds(request) {
|
|
102
|
+
const m = LENGTH_VALUE.exec(String(request ?? ""));
|
|
103
|
+
if (!m) return null;
|
|
104
|
+
const value = Number(m[1]);
|
|
105
|
+
return /^(?:분|min|mins|minutes?)$/iu.test(m[2]) ? value * 60 : value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** The genre a request's own words fix, or null when it names none. */
|
|
109
|
+
export function cuedGenre(request) {
|
|
110
|
+
const text = String(request ?? "");
|
|
111
|
+
const compound = TYPE_COMPOUND.exec(text);
|
|
112
|
+
if (compound) return { genre: "type-led", word: compound[0] };
|
|
113
|
+
for (const [genre, re] of GENRE_CUES) {
|
|
114
|
+
const m = re.exec(text);
|
|
115
|
+
if (m) return { genre, word: m[0] };
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
export const asksVertical = (request) => VERTICAL.test(String(request ?? ""));
|
|
120
|
+
/** The request asks for a calm, quiet or minimal film: the one case the minimal register is for. */
|
|
121
|
+
export const asksMinimal = (request) => MINIMAL_ASKED.test(String(request ?? ""));
|
|
122
|
+
export const asksPreview = (request) => PREVIEW_ASKED.test(String(request ?? ""));
|
|
123
|
+
// High-quality, cinematic or photoreal 3D: the words that send a 3D film to the Blender rung when
|
|
124
|
+
// Blender is installed (scaffold.mjs says so; references/depth.md).
|
|
125
|
+
const PATH_TRACED_ASKED = /고퀄|고품질|하이엔드|시네마틱|실사|포토리얼|레이\s?트레이싱|블렌더|\b(?:high[- ]?(?:quality|end)|cinematic|photo-?real(?:istic)?|path[- ]?trac\w*|ray[- ]?trac\w*|blender|cycles)\b/iu;
|
|
126
|
+
/** The request asks for path-traced quality in its 3D. */
|
|
127
|
+
export const asksPathTraced = (request) => PATH_TRACED_ASKED.test(String(request ?? ""));
|
|
128
|
+
export const asks30fps = (request) => FPS30_ASKED.test(String(request ?? ""));
|
|
129
|
+
export const normUrl = (u) => String(u ?? "").trim().replace(/[.,;:!?]+$/u, "").replace(/\/+$/u, "").replace(/\.git$/u, "").toLowerCase();
|
|
130
|
+
/** Every link in the request, normalized (no trailing slash, punctuation or .git). */
|
|
131
|
+
export const requestUrls = (request) => [...String(request ?? "").matchAll(URL_IN_REQUEST)].map((m) => normUrl(m[0]));
|
|
61
132
|
const PLACEHOLDER = /<[^<>\n]{1,80}>|^\s*(?:todo|tbd|tk|xxx|\.\.\.|…)\s*$/iu;
|
|
62
133
|
|
|
63
134
|
const isText = (v) => typeof v === "string" && v.trim().length > 0;
|
|
@@ -67,8 +138,11 @@ const sentenceCount = (text) => String(text).trim().split(/(?<=[.!?。?!])\s
|
|
|
67
138
|
export function freeTextLeaves(t) {
|
|
68
139
|
const leaves = [["idea", t.idea], ["audience", t.audience], ["channel", t.channel], ["ambition", t.ambition]];
|
|
69
140
|
(Array.isArray(t.beats) ? t.beats : []).forEach((b, i) => {
|
|
70
|
-
for (const k of ["purpose", "onScreen", "motion", "sound"]) leaves.push([`beats[${i}].${k}`, b?.[k]]);
|
|
141
|
+
for (const k of ["purpose", "onScreen", "motion", "sound", "hero"]) leaves.push([`beats[${i}].${k}`, b?.[k]]);
|
|
71
142
|
});
|
|
143
|
+
for (const k of ["expect", "turn", "payoff"]) leaves.push([`story.${k}`, t.story?.[k]]);
|
|
144
|
+
leaves.push(["protagonist.look", t.protagonist?.look]);
|
|
145
|
+
(Array.isArray(t.protagonist?.acts) ? t.protagonist.acts : []).forEach((a, i) => leaves.push([`protagonist.acts[${i}].action`, a?.action]));
|
|
72
146
|
(Array.isArray(t.copy?.lines) ? t.copy.lines : []).forEach((line, i) => leaves.push([`copy.lines[${i}]`, line]));
|
|
73
147
|
(Array.isArray(t.palette) ? t.palette : []).forEach((p, i) => leaves.push([`palette[${i}].role`, p?.role]));
|
|
74
148
|
return leaves.filter(([, v]) => typeof v === "string");
|
|
@@ -131,32 +205,44 @@ function checkCopiedExample(t, examples) {
|
|
|
131
205
|
* Validate a treatment. Returns it with `seed` filled in. Throws TreatmentError(field) on the first
|
|
132
206
|
* problem, in field order, so the model fixes one named thing at a time.
|
|
133
207
|
*/
|
|
134
|
-
export function validateTreatment(t, { examples = shippedExamples() } = {}) {
|
|
208
|
+
export function validateTreatment(t, { examples = shippedExamples(), house = null } = {}) {
|
|
135
209
|
if (!t || typeof t !== "object" || Array.isArray(t)) fail("treatment.json", "must be a JSON object");
|
|
136
210
|
if (t.mode === "cover") fail("mode", "this is a cover treatment: render it with motion.mjs cover --out <dir> (references/cover.md)");
|
|
137
211
|
checkText(t, "request");
|
|
138
212
|
if (!("genre" in t)) fail("genre", "missing");
|
|
139
213
|
if (!GENRES.includes(t.genre)) fail("genre", `must be one of ${GENRES.join(", ")}`);
|
|
214
|
+
const cue = cuedGenre(t.request);
|
|
215
|
+
if (cue && cue.genre !== t.genre) fail("genre", `the request says "${cue.word}", which makes this a ${cue.genre} film`);
|
|
140
216
|
if (!("path" in t)) fail("path", "missing");
|
|
141
217
|
if (!["type", "stage"].includes(t.path)) fail("path", "must be type or stage");
|
|
142
218
|
checkText(t, "pathReason");
|
|
219
|
+
checkStyle(t, house);
|
|
220
|
+
t = { ...t, register: checkRegister(t) };
|
|
143
221
|
checkText(t, "idea", { sentences: 1 });
|
|
144
222
|
checkText(t, "audience");
|
|
145
223
|
checkText(t, "channel");
|
|
146
224
|
if (!("format" in t)) fail("format", "missing");
|
|
147
225
|
if (!FORMATS[t.format]) fail("format", "must be 16:9 or 9:16");
|
|
226
|
+
if (asksVertical(t.request) && t.format !== "9:16") fail("format", "the request asks for a vertical film: 9:16");
|
|
227
|
+
if (!asksVertical(t.request) && t.format === "9:16") fail("format", "9:16 only when the request asks for a vertical film (vertical, 세로, shorts, reels); otherwise 16:9");
|
|
148
228
|
checkText(t, "formatReason");
|
|
149
229
|
if (!("durationSec" in t)) fail("durationSec", "missing");
|
|
150
230
|
if (typeof t.durationSec !== "number" || !Number.isFinite(t.durationSec)) fail("durationSec", "must be a number of seconds");
|
|
151
231
|
if (t.durationSec < 4 || t.durationSec > 90) fail("durationSec", "must be 4-90");
|
|
152
|
-
|
|
153
|
-
fail("durationSec", `a ${t.genre} film runs at least 10 s unless the user asked for a length; length comes from the arc`);
|
|
154
|
-
}
|
|
232
|
+
checkLength(t);
|
|
155
233
|
if ("fps" in t && t.fps !== 60 && t.fps !== 30) fail("fps", "60 (the default) or 30");
|
|
234
|
+
if (t.fps === 30 && !asks30fps(t.request)) fail("fps", "60; 30 only when the request asks for 30 fps");
|
|
235
|
+
if ("preview" in t && typeof t.preview !== "boolean") fail("preview", "true or false");
|
|
236
|
+
if (t.preview === true && !asksPreview(t.request)) fail("preview", "true only when the request asks for a preview or storyboard first");
|
|
156
237
|
checkBeats(t);
|
|
157
238
|
if ("posterBeat" in t && !(Number.isInteger(t.posterBeat) && t.posterBeat >= 0 && t.posterBeat < t.beats.length)) fail("posterBeat", "must be a beat index");
|
|
239
|
+
checkStory(t);
|
|
240
|
+
checkProtagonist(t);
|
|
158
241
|
checkSubject(t);
|
|
242
|
+
checkProduct(t);
|
|
159
243
|
checkDevices(t);
|
|
244
|
+
checkDepth(t);
|
|
245
|
+
checkWorld(t);
|
|
160
246
|
checkTypePlan(t);
|
|
161
247
|
checkPalette(t);
|
|
162
248
|
checkSound(t);
|
|
@@ -175,6 +261,35 @@ function seedOf(text) {
|
|
|
175
261
|
return h;
|
|
176
262
|
}
|
|
177
263
|
|
|
264
|
+
function checkStyle(t, house) {
|
|
265
|
+
if (!("style" in t)) fail("style", `missing: one of ${STYLE_IDS.join(", ")} (references/studio.md)`);
|
|
266
|
+
if (!STYLE_IDS.includes(t.style)) fail("style", `must be one of ${STYLE_IDS.join(", ")}`);
|
|
267
|
+
const pick = pickStyle({ request: t.request, copy: Array.isArray(t.copy?.lines) ? t.copy.lines : [], path: t.path, house, genre: t.genre });
|
|
268
|
+
if (t.style !== pick.id) fail("style", `${pick.reason}; this film takes the ${pick.id} style`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Maximal is the default: a film is rich unless the request asks for calm or minimal, and then it is
|
|
272
|
+
// minimal. The register is the request's, like the format, never a choice the model makes alone.
|
|
273
|
+
function checkRegister(t) {
|
|
274
|
+
const register = t.register ?? "rich";
|
|
275
|
+
if (!REGISTERS.includes(register)) fail("register", "rich (the default) or minimal");
|
|
276
|
+
const calm = asksMinimal(t.request);
|
|
277
|
+
if (register === "minimal" && !calm) fail("register", "minimal only when the request asks for a calm, quiet or minimal film; the default is rich (studio.md, Register)");
|
|
278
|
+
if (register === "rich" && calm) fail("register", "the request asks for a calm or minimal film: register minimal");
|
|
279
|
+
return register;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function checkLength(t) {
|
|
283
|
+
const asked = askedSeconds(t.request);
|
|
284
|
+
if (asked !== null && LENGTH_ASKED.test(t.request)) {
|
|
285
|
+
if (Math.abs(t.durationSec - asked) > Math.max(1, asked * 0.1)) fail("durationSec", `the request asks for ${asked} s; keep the film within 10 % of it`);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const lines = Array.isArray(t.copy?.lines) ? t.copy.lines.length : 1;
|
|
289
|
+
const [lo, hi] = t.genre === "type-led" ? typeLedBand(lines) : GENRE_BANDS[t.genre];
|
|
290
|
+
if (t.durationSec < lo - 1e-9 || t.durationSec > hi + 1e-9) fail("durationSec", `a ${t.genre} film runs ${lo}-${hi} s unless the user names a length (${t.durationSec} s)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
178
293
|
function checkBeats(t) {
|
|
179
294
|
if (!("beats" in t)) fail("beats", "missing");
|
|
180
295
|
if (!Array.isArray(t.beats) || t.beats.length === 0) fail("beats", "must be a non-empty array");
|
|
@@ -186,6 +301,7 @@ function checkBeats(t) {
|
|
|
186
301
|
if (!isText(b[k])) fail(`${f}.${k}`, "must be a non-empty string");
|
|
187
302
|
if (PLACEHOLDER.test(b[k])) fail(`${f}.${k}`, `placeholder value ${JSON.stringify(b[k])}`);
|
|
188
303
|
}
|
|
304
|
+
if (t.path === "stage") checkShot(t, b, i, f);
|
|
189
305
|
if (b.t1 - b.t0 < MIN_BEAT_SEC - 1e-9) fail(f, `a beat lasts at least ${MIN_BEAT_SEC} s (${(b.t1 - b.t0).toFixed(2)} s)`);
|
|
190
306
|
const prevEnd = i === 0 ? 0 : t.beats[i - 1].t1;
|
|
191
307
|
if (b.t0 - prevEnd > MAX_GAP_SEC + 1e-9) fail(f, `gap of ${(b.t0 - prevEnd).toFixed(2)} s before this beat (max ${MAX_GAP_SEC} s)`);
|
|
@@ -196,6 +312,133 @@ function checkBeats(t) {
|
|
|
196
312
|
const lines = Array.isArray(t.copy?.lines) ? t.copy.lines.length : 0;
|
|
197
313
|
const need = t.genre === "type-led" ? Math.max(1, lines) : ARC_STAGES[t.genre];
|
|
198
314
|
if (t.beats.length < need) fail("beats", `a ${t.genre} arc needs at least ${need} beats (${t.beats.length} given); length comes from the arc`);
|
|
315
|
+
if (t.path === "stage") checkVariety(t);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Composition changes with the story: two adjacent shots never share a layout, and a film of four or
|
|
319
|
+
// more shots uses at least three, so the eye is not parked on one split for the whole film.
|
|
320
|
+
function checkVariety(t) {
|
|
321
|
+
for (let i = 1; i < t.beats.length; i++) {
|
|
322
|
+
if (t.beats[i].layout === t.beats[i - 1].layout) fail(`beats[${i}].layout`, `shots ${i} and ${i + 1} both use ${t.beats[i].layout}; adjacent shots change layout (studio.md, Shots and layouts)`);
|
|
323
|
+
}
|
|
324
|
+
const used = new Set(t.beats.map((b) => b.layout));
|
|
325
|
+
if (t.beats.length >= 4 && used.size < 3) fail("beats", `${t.beats.length} shots use only ${[...used].join(" and ")}; a film of four or more shots uses at least three layouts`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// The story: what the viewer expects, the turn they do not, the shot where it lands, and the payoff.
|
|
329
|
+
// A genre arc alone is an information order, not a story.
|
|
330
|
+
function checkStory(t) {
|
|
331
|
+
if (!("story" in t)) fail("story", "missing: { expect, turn, beat, payoff }: what the viewer expects, what happens instead, the shot where it lands, what the ending gives (treatment.md, Story)");
|
|
332
|
+
const s = t.story;
|
|
333
|
+
if (!s || typeof s !== "object") fail("story", "must be an object { expect, turn, beat, payoff }");
|
|
334
|
+
for (const k of ["expect", "turn", "payoff"]) {
|
|
335
|
+
if (!isText(s[k])) fail(`story.${k}`, "must be a non-empty string");
|
|
336
|
+
if (PLACEHOLDER.test(s[k])) fail(`story.${k}`, `placeholder value ${JSON.stringify(s[k])}`);
|
|
337
|
+
if (sentenceCount(s[k]) > 2) fail(`story.${k}`, "at most 2 sentences");
|
|
338
|
+
}
|
|
339
|
+
if (normalizeText(s.turn) === normalizeText(s.expect)) fail("story.turn", "the turn is what the viewer does not expect; it cannot equal story.expect");
|
|
340
|
+
if (!Number.isInteger(s.beat) || s.beat < 1 || s.beat >= t.beats.length) fail("story.beat", `the index of the shot where the turn lands: after the opening, before the end (1-${t.beats.length - 1})`);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// The protagonist: one drawn character or hero object that acts across the film (stage path).
|
|
344
|
+
function checkProtagonist(t) {
|
|
345
|
+
if (t.path !== "stage") return;
|
|
346
|
+
if (!("protagonist" in t)) fail("protagonist", "missing: { name, kind, look, acts[] }: the character or hero object that acts across the film (treatment.md, Protagonist)");
|
|
347
|
+
const p = t.protagonist;
|
|
348
|
+
if (!p || typeof p !== "object") fail("protagonist", "must be an object { name, kind, look, acts }");
|
|
349
|
+
if (!isText(p.name) || PLACEHOLDER.test(p.name)) fail("protagonist.name", "must name it");
|
|
350
|
+
if (!PROTAGONIST_KINDS.includes(p.kind)) fail("protagonist.kind", `must be ${PROTAGONIST_KINDS.join(" or ")}`);
|
|
351
|
+
if (!isText(p.look) || PLACEHOLDER.test(p.look)) fail("protagonist.look", "say what it looks like on screen: shape, material, colour role, scale in the frame");
|
|
352
|
+
if (!Array.isArray(p.acts) || !p.acts.length) fail("protagonist.acts", "list what it does, shot by shot: [{ beat, action }]");
|
|
353
|
+
const seen = new Set();
|
|
354
|
+
p.acts.forEach((a, i) => {
|
|
355
|
+
if (!Number.isInteger(a?.beat) || a.beat < 0 || a.beat >= t.beats.length) fail(`protagonist.acts[${i}].beat`, "must be a beat index");
|
|
356
|
+
if (seen.has(a.beat)) fail(`protagonist.acts[${i}].beat`, "one act per shot");
|
|
357
|
+
seen.add(a.beat);
|
|
358
|
+
if (!isText(a.action) || PLACEHOLDER.test(a.action)) fail(`protagonist.acts[${i}].action`, "say what it does in this shot, as a viewer would see it");
|
|
359
|
+
});
|
|
360
|
+
const covered = beatSpan(t, [...seen]);
|
|
361
|
+
if (covered < 0.5 * t.durationSec - 1e-9) fail("protagonist.acts", `it acts for ${covered.toFixed(1)} s, under half the film; it carries the film, not one shot`);
|
|
362
|
+
if (!seen.has(t.story.beat)) fail("protagonist.acts", `the turn lands at shot ${t.story.beat + 1}, and the protagonist has no act there`);
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// A named product, repository or site: its real logo, images, UI and words, never invented ones. The
|
|
366
|
+
// request's link makes it required; `motion.mjs assets` fetches what it can reach.
|
|
367
|
+
function checkProduct(t) {
|
|
368
|
+
const urls = requestUrls(t.request);
|
|
369
|
+
if (!("product" in t)) {
|
|
370
|
+
if (urls.length) fail("product", `missing: the request links ${urls[0]}; fetch its real assets with motion.mjs assets --out <dir> --url <link> and list them (treatment.md, Named products)`);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const p = t.product;
|
|
374
|
+
if (!p || typeof p !== "object") fail("product", "must be an object { name, url, assets }");
|
|
375
|
+
if (!isText(p.name) || PLACEHOLDER.test(p.name)) fail("product.name", "must name the product");
|
|
376
|
+
if (urls.length && !urls.includes(normUrl(p.url))) fail("product.url", `must be the link the request gives (${urls.join(", ")})`);
|
|
377
|
+
if ("unreachable" in p) {
|
|
378
|
+
if (!isText(p.unreachable)) fail("product.unreachable", "say what could not be reached and why");
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
if (!Array.isArray(p.assets) || !p.assets.length) fail("product.assets", "list the real assets the film uses: [{ path, role, source, sha256 }]; when none can be reached set unreachable and ask the user");
|
|
382
|
+
p.assets.forEach((a, i) => {
|
|
383
|
+
const f = `product.assets[${i}]`;
|
|
384
|
+
if (typeof a?.path !== "string" || !a.path.startsWith("stage/assets/") || a.path.split("/").includes("..")) fail(`${f}.path`, "must be a file under stage/assets/, relative to the output directory");
|
|
385
|
+
if (!ASSET_ROLES.includes(a.role)) fail(`${f}.role`, `must be one of ${ASSET_ROLES.join(", ")}`);
|
|
386
|
+
if (!["fetched", "user"].includes(a.source)) fail(`${f}.source`, "must be fetched (by motion.mjs assets) or user (supplied)");
|
|
387
|
+
if (!/^[0-9a-f]{64}$/u.test(a.sha256 ?? "")) fail(`${f}.sha256`, "must be the file's SHA-256");
|
|
388
|
+
});
|
|
389
|
+
if (t.subject?.source === "invented") fail("subject.source", "the product is real: the subject is the product (source user), its specifics from its own words");
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// The 3D route: a depth3d subject is drawn with the pinned three.js on the GPU rung (references/depth.md).
|
|
393
|
+
function checkDepth(t) {
|
|
394
|
+
if ("depth" in t && !["2d", "3d"].includes(t.depth)) fail("depth", "2d (the default) or 3d");
|
|
395
|
+
const solid = Array.isArray(t.visualDevices) && t.visualDevices.some((d) => d.kind === "depth3d" && d.role === "subject");
|
|
396
|
+
if (t.path !== "stage") {
|
|
397
|
+
if (t.depth === "3d") fail("depth", "3d is a stage-path route");
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
if (solid && t.depth !== "3d") fail("depth", "the subject is a depth3d device: set depth to 3d, so the film takes the 3D route (references/depth.md)");
|
|
401
|
+
if (t.depth === "3d" && !solid) fail("depth", "3d needs the subject drawn as a depth3d device (role subject)");
|
|
402
|
+
if ("render3d" in t && !["three", "blender"].includes(t.render3d)) fail("render3d", "three (the GPU rung, the default) or blender (the offline Cycles rung)");
|
|
403
|
+
if (t.render3d === "blender" && t.depth !== "3d") fail("render3d", "the Blender rung renders a 3D world: set depth to 3d with a depth3d subject");
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// A rich stage film plans its density before any page exists: the depth layers of its world, the
|
|
407
|
+
// labelled parts the viewer reads in it and the shots where the camera moves (references/studio.md).
|
|
408
|
+
function checkWorld(t) {
|
|
409
|
+
if (t.path !== "stage" || t.register !== "rich") return;
|
|
410
|
+
const w = t.world;
|
|
411
|
+
if (!w || typeof w !== "object") fail("world", "missing: { layers, labels, camera }: a rich film plans its layered world, its labelled parts and its camera moves (studio.md, Register)");
|
|
412
|
+
if (!Array.isArray(w.layers) || w.layers.filter(isText).length < RICH_WORLD.layers) fail("world.layers", `at least ${RICH_WORLD.layers} depth layers, back to front, each saying what lives there`);
|
|
413
|
+
if (!Array.isArray(w.labels) || w.labels.length < RICH_WORLD.labels) fail("world.labels", `at least ${RICH_WORLD.labels} labelled parts the viewer reads on screen (real names from the product when there is one)`);
|
|
414
|
+
w.labels.forEach((l, i) => {
|
|
415
|
+
if (!isText(l) || PLACEHOLDER.test(l)) fail(`world.labels[${i}]`, "the label as it appears on screen");
|
|
416
|
+
if (l.trim().split(/\s+/u).length > RICH_WORLD.labelWords) fail(`world.labels[${i}]`, `a label is at most ${RICH_WORLD.labelWords} words; longer text is copy`);
|
|
417
|
+
});
|
|
418
|
+
if (!Array.isArray(w.camera)) fail("world.camera", "list the camera moves: [{ beat, move }]");
|
|
419
|
+
const moving = new Set();
|
|
420
|
+
w.camera.forEach((c, i) => {
|
|
421
|
+
if (!Number.isInteger(c?.beat) || c.beat < 0 || c.beat >= t.beats.length) fail(`world.camera[${i}].beat`, "must be a beat index");
|
|
422
|
+
if (!isText(c.move) || PLACEHOLDER.test(c.move) || LOCKED_CAMERA.test(c.move)) fail(`world.camera[${i}].move`, "name the move (push-in, truck, crane, orbit, rack focus); a locked shot is not listed");
|
|
423
|
+
moving.add(c.beat);
|
|
424
|
+
});
|
|
425
|
+
if (moving.size < RICH_WORLD.cameraShare * t.beats.length - 1e-9) fail("world.camera", `the camera moves in ${moving.size} of ${t.beats.length} shots; a rich film moves it in at least half (keep the flash audit with slow light changes, not a locked camera)`);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// A stage shot: which layout its type and subject take, the one move the eye follows, and how it hands
|
|
429
|
+
// off to the next shot (a word from the style's transitions; the last shot ends).
|
|
430
|
+
function checkShot(t, b, i, f) {
|
|
431
|
+
const style = readStyle(t.style);
|
|
432
|
+
if (!LAYOUT_IDS.includes(b.layout)) fail(`${f}.layout`, `must be one of ${LAYOUT_IDS.join(", ")}`);
|
|
433
|
+
if (!isText(b.hero) || PLACEHOLDER.test(b.hero)) fail(`${f}.hero`, "name the one move the eye follows in this shot");
|
|
434
|
+
const last = i === t.beats.length - 1;
|
|
435
|
+
if (last && b.transition !== "end") fail(`${f}.transition`, "the last shot's transition is end");
|
|
436
|
+
if (!last && !style.transitions.includes(b.transition)) fail(`${f}.transition`, `the ${style.id} style hands off with ${style.transitions.join(", ")}`);
|
|
437
|
+
if ("lines" in b) {
|
|
438
|
+
const n = Array.isArray(t.copy?.lines) ? t.copy.lines.length : 0;
|
|
439
|
+
if (!Array.isArray(b.lines) || b.lines.some((k) => !Number.isInteger(k) || k < 0 || k >= n)) fail(`${f}.lines`, "the indices of the copy lines this shot shows");
|
|
440
|
+
if (b.layout === "full" && b.lines.length) fail(`${f}.lines`, "a full shot has no type zone, so it shows no copy");
|
|
441
|
+
}
|
|
199
442
|
}
|
|
200
443
|
|
|
201
444
|
function checkSubject(t) {
|
|
@@ -226,6 +469,11 @@ function checkDevices(t) {
|
|
|
226
469
|
if (!["subject", "support", "texture"].includes(d.role)) fail(`${f}.role`, "must be subject, support or texture");
|
|
227
470
|
if (TEXTURE_KINDS.includes(d.kind) && d.role !== "texture") fail(`${f}.role`, `${d.kind} is always texture`);
|
|
228
471
|
if (!Array.isArray(d.beats) || !d.beats.length || d.beats.some((b) => !Number.isInteger(b) || b < 0 || b >= t.beats.length)) fail(`${f}.beats`, "must list beat indices");
|
|
472
|
+
if (d.kind === "screen") {
|
|
473
|
+
const assets = Array.isArray(t.product?.assets) ? t.product.assets : [];
|
|
474
|
+
const real = assets.find((a) => a.path === d.asset);
|
|
475
|
+
if (!real || !SCREEN_ROLES.has(real.role)) fail(`${f}.asset`, "a product screen shows the product's real screenshot, UI or README image: name one listed in product.assets; never draw an invented one");
|
|
476
|
+
}
|
|
229
477
|
});
|
|
230
478
|
if (t.path !== "stage") return;
|
|
231
479
|
const subjects = t.visualDevices.filter((d) => d.role === "subject");
|
|
@@ -241,12 +489,28 @@ function checkTypePlan(t) {
|
|
|
241
489
|
const p = t.typePlan;
|
|
242
490
|
if (!p || typeof p !== "object") fail("typePlan", "must be an object");
|
|
243
491
|
if (!Array.isArray(p.faces) || !p.faces.length || p.faces.some((f) => !FACES.includes(f))) fail("typePlan.faces", `faces come from the verified set: ${FACES.join(", ")}`);
|
|
492
|
+
if (t.path === "stage") {
|
|
493
|
+
const families = styleFamilies(readStyle(t.style));
|
|
494
|
+
const off = p.faces.find((f) => !families.has(f));
|
|
495
|
+
if (off) fail("typePlan.faces", `${off} is not a face of the ${t.style} style (${[...families].join(", ")})`);
|
|
496
|
+
}
|
|
244
497
|
if (!isText(p.hierarchy)) fail("typePlan.hierarchy", "must be a non-empty string");
|
|
245
498
|
if (!Number.isInteger(p.maxWordsOnScreen) || p.maxWordsOnScreen < 1) fail("typePlan.maxWordsOnScreen", "must be a positive integer");
|
|
246
499
|
if ("index" in p && typeof p.index !== "boolean") fail("typePlan.index", "must be true or false");
|
|
247
500
|
}
|
|
248
501
|
|
|
249
502
|
function checkPalette(t) {
|
|
503
|
+
if (t.path === "stage") {
|
|
504
|
+
if (!("palette" in t)) return;
|
|
505
|
+
const rich = t.register === "rich";
|
|
506
|
+
const allowed = new Set(styleColors(readStyle(t.style), { rich }));
|
|
507
|
+
if (!Array.isArray(t.palette) || t.palette.length > (rich ? 14 : 9)) fail("palette", "a list of the style's colours with their roles");
|
|
508
|
+
t.palette.forEach((p, i) => {
|
|
509
|
+
if (!allowed.has(String(p?.color ?? "").toLowerCase())) fail(`palette[${i}].color`, `${p?.color} is not a colour of the ${t.style} style${rich ? " or one of its role accents" : " (a minimal film keeps to the core palette)"}; the stage palette comes from the style`);
|
|
510
|
+
if (!isText(p.role)) fail(`palette[${i}].role`, "must be a non-empty string");
|
|
511
|
+
});
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
250
514
|
if (!("palette" in t)) fail("palette", "missing");
|
|
251
515
|
if (!Array.isArray(t.palette) || t.palette.length < 3 || t.palette.length > 6) fail("palette", "needs 3-6 colours, each with a role");
|
|
252
516
|
t.palette.forEach((p, i) => {
|
|
@@ -259,11 +523,13 @@ function checkSound(t) {
|
|
|
259
523
|
if (!("sound" in t)) fail("sound", "missing");
|
|
260
524
|
const s = t.sound;
|
|
261
525
|
if (!s || typeof s !== "object") fail("sound", "must be an object");
|
|
262
|
-
if (!["generated", "supplied", "authored", "none"].includes(s.mode)) fail("sound.mode", "must be generated, supplied, authored or none");
|
|
526
|
+
if (!["score", "generated", "supplied", "authored", "none"].includes(s.mode)) fail("sound.mode", "must be score, generated, supplied, authored or none");
|
|
263
527
|
if (!isText(s.plan)) fail("sound.plan", "must be a non-empty string");
|
|
264
528
|
if (s.mode === "none" && !SILENCE_ASKED.test(t.request) && !MUTED_CHANNEL.test(t.channel)) {
|
|
265
|
-
fail("sound.mode", "none only when the user asked for silence or the channel plays muted by design; the default is a
|
|
529
|
+
fail("sound.mode", "none only when the user asked for silence or the channel plays muted by design; the default is a score");
|
|
266
530
|
}
|
|
531
|
+
if (s.mode === "generated" && t.register === "rich") fail("sound.mode", "a rich film scores its own music (mode score); the style's preset beds are for minimal films (sound.md)");
|
|
532
|
+
if ("genre" in s && !SCORE_GENRES.includes(s.genre)) fail("sound.genre", `must be one of ${SCORE_GENRES.join(", ")}`);
|
|
267
533
|
if (s.mode === "generated") {
|
|
268
534
|
try {
|
|
269
535
|
normalizePalette(s.palette);
|
|
@@ -271,10 +537,29 @@ function checkSound(t) {
|
|
|
271
537
|
if (error instanceof SoundError) fail(error.field.startsWith("sound.") ? error.field : `sound.palette.${error.field}`, error.message);
|
|
272
538
|
throw error;
|
|
273
539
|
}
|
|
540
|
+
const beds = styleBeds(readStyle(t.style));
|
|
541
|
+
const same = (b) => ["timbre", "key", "mode", "tempo"].every((k) => String(s.palette[k]).toLowerCase() === String(b[k]).toLowerCase());
|
|
542
|
+
if (!beds.some(same)) fail("sound.palette", `pick one of the ${t.style} style's beds: ${beds.map((b) => `${b.timbre} ${b.key} ${b.mode} ${b.tempo} BPM`).join("; ")}`);
|
|
274
543
|
}
|
|
544
|
+
if ((s.mode === "generated" || s.mode === "score") && t.path === "stage" && !("events" in s)) fail("sound.events", "list the on-screen events that make a sound: [{ t, on, kind }], at least 2, half of them away from the cuts (sound.md, Events)");
|
|
545
|
+
if ("events" in s) checkEvents(t, s.events);
|
|
275
546
|
if ((s.mode === "supplied" || s.mode === "authored") && !isText(s.file)) fail("sound.file", `a ${s.mode} track needs its file path`);
|
|
276
547
|
}
|
|
277
548
|
|
|
549
|
+
function checkEvents(t, events) {
|
|
550
|
+
if (!Array.isArray(events) || events.length < 2) fail("sound.events", "at least 2 on-screen events that make a sound: [{ t, on, kind }]");
|
|
551
|
+
const cuts = t.beats.slice(1).map((b) => b.t0);
|
|
552
|
+
let offCut = 0;
|
|
553
|
+
events.forEach((e, i) => {
|
|
554
|
+
const f = `sound.events[${i}]`;
|
|
555
|
+
if (typeof e?.t !== "number" || !(e.t > 0 && e.t < t.durationSec)) fail(`${f}.t`, `the second the event happens on screen, inside the film (0-${t.durationSec})`);
|
|
556
|
+
if (!isText(e.on) || PLACEHOLDER.test(e.on)) fail(`${f}.on`, "what happens on screen at that moment, as a viewer would see it");
|
|
557
|
+
if (!EVENT_SOUNDS.includes(e.kind)) fail(`${f}.kind`, `must be one of ${EVENT_SOUNDS.join(", ")}`);
|
|
558
|
+
if (cuts.every((c) => Math.abs(c - e.t) >= EVENT_OFF_CUT_SEC - 1e-9)) offCut += 1;
|
|
559
|
+
});
|
|
560
|
+
if (offCut * 2 < events.length) fail("sound.events", `${events.length - offCut} of ${events.length} events sit on a cut; at least half sync to something moving inside a shot`);
|
|
561
|
+
}
|
|
562
|
+
|
|
278
563
|
function checkCopy(t) {
|
|
279
564
|
if (!("copy" in t)) fail("copy", "missing");
|
|
280
565
|
const c = t.copy;
|
|
@@ -318,6 +603,21 @@ function checkPath(t) {
|
|
|
318
603
|
if (t.copy.source !== "user" && !typeLedCue(t.request)) fail("path", "the type path is for films whose words are the film: user-supplied copy or a type-led cue in the request");
|
|
319
604
|
}
|
|
320
605
|
|
|
606
|
+
/**
|
|
607
|
+
* The product's files on disk: each listed asset exists and still has its hash. An unreachable product
|
|
608
|
+
* is exit 22: the film stops and the reply asks the user for the logo, screens and words.
|
|
609
|
+
*/
|
|
610
|
+
export function checkProductFiles(out, t) {
|
|
611
|
+
const p = t.product;
|
|
612
|
+
if (!p) return;
|
|
613
|
+
if (p.unreachable) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${p.name}'s real assets could not be reached (${p.unreachable}); ask the user for its logo, a screenshot or the README images, put them in ${path.join(out, "stage", "assets")} and list them; never draw invented product screens`);
|
|
614
|
+
for (const a of p.assets) {
|
|
615
|
+
const file = path.join(out, a.path);
|
|
616
|
+
if (!existsSync(file)) throw new BlockedError(EXIT.BLOCKED_ASSET_MISSING, `BLOCKED_ASSET_MISSING: ${a.path} is listed in product.assets and missing; fetch or supply it, never draw a stand-in`);
|
|
617
|
+
if (createHash("sha256").update(readFileSync(file)).digest("hex") !== a.sha256) throw new BlockedError(EXIT.STAGE_CONTRACT_ERROR, `STAGE_CONTRACT_ERROR: ${a.path} does not match its sha256 in product.assets; list it again after changing it`);
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
|
|
321
621
|
/**
|
|
322
622
|
* Load and validate `<out>/treatment.json` for a render on `pathName`. The first valid treatment of
|
|
323
623
|
* an output directory is kept in .run/treatment.first.json for the done-check's downgrade test.
|
|
@@ -331,7 +631,8 @@ export function loadTreatment(out, { pathName } = {}) {
|
|
|
331
631
|
} catch (error) {
|
|
332
632
|
fail("treatment.json", `not valid JSON (${error.message})`);
|
|
333
633
|
}
|
|
334
|
-
const treatment = validateTreatment(raw);
|
|
634
|
+
const treatment = validateTreatment(raw, { house: readHouse(out) });
|
|
635
|
+
checkProductFiles(out, treatment);
|
|
335
636
|
if (pathName && treatment.path !== pathName) {
|
|
336
637
|
fail("path", treatment.path === "stage" ? "this treatment takes the stage path: render it with the stage subcommand" : "this treatment takes the type path: render it with make <brief.json>");
|
|
337
638
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License
|
|
2
|
+
|
|
3
|
+
Copyright © 2010-2024 three.js authors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* https://github.com/google/model-viewer/blob/master/packages/model-viewer/src/three-components/EnvironmentScene.ts
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BackSide,
|
|
7
|
+
BoxGeometry,
|
|
8
|
+
Mesh,
|
|
9
|
+
MeshBasicMaterial,
|
|
10
|
+
MeshStandardMaterial,
|
|
11
|
+
PointLight,
|
|
12
|
+
Scene,
|
|
13
|
+
} from 'three';
|
|
14
|
+
|
|
15
|
+
class RoomEnvironment extends Scene {
|
|
16
|
+
|
|
17
|
+
constructor() {
|
|
18
|
+
|
|
19
|
+
super();
|
|
20
|
+
|
|
21
|
+
const geometry = new BoxGeometry();
|
|
22
|
+
geometry.deleteAttribute( 'uv' );
|
|
23
|
+
|
|
24
|
+
const roomMaterial = new MeshStandardMaterial( { side: BackSide } );
|
|
25
|
+
const boxMaterial = new MeshStandardMaterial();
|
|
26
|
+
|
|
27
|
+
const mainLight = new PointLight( 0xffffff, 900, 28, 2 );
|
|
28
|
+
mainLight.position.set( 0.418, 16.199, 0.300 );
|
|
29
|
+
this.add( mainLight );
|
|
30
|
+
|
|
31
|
+
const room = new Mesh( geometry, roomMaterial );
|
|
32
|
+
room.position.set( - 0.757, 13.219, 0.717 );
|
|
33
|
+
room.scale.set( 31.713, 28.305, 28.591 );
|
|
34
|
+
this.add( room );
|
|
35
|
+
|
|
36
|
+
const box1 = new Mesh( geometry, boxMaterial );
|
|
37
|
+
box1.position.set( - 10.906, 2.009, 1.846 );
|
|
38
|
+
box1.rotation.set( 0, - 0.195, 0 );
|
|
39
|
+
box1.scale.set( 2.328, 7.905, 4.651 );
|
|
40
|
+
this.add( box1 );
|
|
41
|
+
|
|
42
|
+
const box2 = new Mesh( geometry, boxMaterial );
|
|
43
|
+
box2.position.set( - 5.607, - 0.754, - 0.758 );
|
|
44
|
+
box2.rotation.set( 0, 0.994, 0 );
|
|
45
|
+
box2.scale.set( 1.970, 1.534, 3.955 );
|
|
46
|
+
this.add( box2 );
|
|
47
|
+
|
|
48
|
+
const box3 = new Mesh( geometry, boxMaterial );
|
|
49
|
+
box3.position.set( 6.167, 0.857, 7.803 );
|
|
50
|
+
box3.rotation.set( 0, 0.561, 0 );
|
|
51
|
+
box3.scale.set( 3.927, 6.285, 3.687 );
|
|
52
|
+
this.add( box3 );
|
|
53
|
+
|
|
54
|
+
const box4 = new Mesh( geometry, boxMaterial );
|
|
55
|
+
box4.position.set( - 2.017, 0.018, 6.124 );
|
|
56
|
+
box4.rotation.set( 0, 0.333, 0 );
|
|
57
|
+
box4.scale.set( 2.002, 4.566, 2.064 );
|
|
58
|
+
this.add( box4 );
|
|
59
|
+
|
|
60
|
+
const box5 = new Mesh( geometry, boxMaterial );
|
|
61
|
+
box5.position.set( 2.291, - 0.756, - 2.621 );
|
|
62
|
+
box5.rotation.set( 0, - 0.286, 0 );
|
|
63
|
+
box5.scale.set( 1.546, 1.552, 1.496 );
|
|
64
|
+
this.add( box5 );
|
|
65
|
+
|
|
66
|
+
const box6 = new Mesh( geometry, boxMaterial );
|
|
67
|
+
box6.position.set( - 2.193, - 0.369, - 5.547 );
|
|
68
|
+
box6.rotation.set( 0, 0.516, 0 );
|
|
69
|
+
box6.scale.set( 3.875, 3.487, 2.986 );
|
|
70
|
+
this.add( box6 );
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
// -x right
|
|
74
|
+
const light1 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
75
|
+
light1.position.set( - 16.116, 14.37, 8.208 );
|
|
76
|
+
light1.scale.set( 0.1, 2.428, 2.739 );
|
|
77
|
+
this.add( light1 );
|
|
78
|
+
|
|
79
|
+
// -x left
|
|
80
|
+
const light2 = new Mesh( geometry, createAreaLightMaterial( 50 ) );
|
|
81
|
+
light2.position.set( - 16.109, 18.021, - 8.207 );
|
|
82
|
+
light2.scale.set( 0.1, 2.425, 2.751 );
|
|
83
|
+
this.add( light2 );
|
|
84
|
+
|
|
85
|
+
// +x
|
|
86
|
+
const light3 = new Mesh( geometry, createAreaLightMaterial( 17 ) );
|
|
87
|
+
light3.position.set( 14.904, 12.198, - 1.832 );
|
|
88
|
+
light3.scale.set( 0.15, 4.265, 6.331 );
|
|
89
|
+
this.add( light3 );
|
|
90
|
+
|
|
91
|
+
// +z
|
|
92
|
+
const light4 = new Mesh( geometry, createAreaLightMaterial( 43 ) );
|
|
93
|
+
light4.position.set( - 0.462, 8.89, 14.520 );
|
|
94
|
+
light4.scale.set( 4.38, 5.441, 0.088 );
|
|
95
|
+
this.add( light4 );
|
|
96
|
+
|
|
97
|
+
// -z
|
|
98
|
+
const light5 = new Mesh( geometry, createAreaLightMaterial( 20 ) );
|
|
99
|
+
light5.position.set( 3.235, 11.486, - 12.541 );
|
|
100
|
+
light5.scale.set( 2.5, 2.0, 0.1 );
|
|
101
|
+
this.add( light5 );
|
|
102
|
+
|
|
103
|
+
// +y
|
|
104
|
+
const light6 = new Mesh( geometry, createAreaLightMaterial( 100 ) );
|
|
105
|
+
light6.position.set( 0.0, 20.0, 0.0 );
|
|
106
|
+
light6.scale.set( 1.0, 0.1, 1.0 );
|
|
107
|
+
this.add( light6 );
|
|
108
|
+
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
|
|
113
|
+
const resources = new Set();
|
|
114
|
+
|
|
115
|
+
this.traverse( ( object ) => {
|
|
116
|
+
|
|
117
|
+
if ( object.isMesh ) {
|
|
118
|
+
|
|
119
|
+
resources.add( object.geometry );
|
|
120
|
+
resources.add( object.material );
|
|
121
|
+
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
} );
|
|
125
|
+
|
|
126
|
+
for ( const resource of resources ) {
|
|
127
|
+
|
|
128
|
+
resource.dispose();
|
|
129
|
+
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function createAreaLightMaterial( intensity ) {
|
|
137
|
+
|
|
138
|
+
const material = new MeshBasicMaterial();
|
|
139
|
+
material.color.setScalar( intensity );
|
|
140
|
+
return material;
|
|
141
|
+
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export { RoomEnvironment };
|