@waits/cadence 0.2.0
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/LICENSE +21 -0
- package/MOTION.md +70 -0
- package/README.md +109 -0
- package/bin/cli.mjs +21 -0
- package/package.json +59 -0
- package/prompts/art/compose.ts +18 -0
- package/prompts/art/fantasy.ts +15 -0
- package/prompts/art/landmarks.ts +49 -0
- package/prompts/art/style.ts +20 -0
- package/public/backgrounds/barton-springs.png +0 -0
- package/public/backgrounds/capitol.png +0 -0
- package/public/backgrounds/congress.png +0 -0
- package/public/backgrounds/enchanted-rock.png +0 -0
- package/public/backgrounds/hamilton-pool.png +0 -0
- package/public/backgrounds/mount-bonnell.png +0 -0
- package/public/backgrounds/pennybacker.png +0 -0
- package/public/backgrounds/ut-tower.png +0 -0
- package/scripts/_pkg.ts +28 -0
- package/scripts/audit.ts +69 -0
- package/scripts/changes.ts +70 -0
- package/scripts/check-motion.ts +37 -0
- package/scripts/cli.ts +79 -0
- package/scripts/generate-art.ts +72 -0
- package/scripts/guide.ts +114 -0
- package/scripts/make.ts +76 -0
- package/scripts/redesign.ts +83 -0
- package/scripts/render.ts +62 -0
- package/scripts/smoke.ts +43 -0
- package/scripts/sync-skill.ts +43 -0
- package/scripts/theme.ts +97 -0
- package/src/Root.tsx +51 -0
- package/src/adapters/index.ts +72 -0
- package/src/adapters/parse.ts +65 -0
- package/src/adapters/types.ts +24 -0
- package/src/brand/fonts.ts +74 -0
- package/src/brand/tokens.ts +31 -0
- package/src/brand/tone.ts +25 -0
- package/src/code/highlight.ts +90 -0
- package/src/components/Background.tsx +75 -0
- package/src/components/Changelog.tsx +17 -0
- package/src/components/ChangelogScene.tsx +117 -0
- package/src/components/CodeWindow.tsx +106 -0
- package/src/components/ContactSheet.tsx +44 -0
- package/src/components/Headline.tsx +73 -0
- package/src/components/MotionReel.tsx +108 -0
- package/src/components/panels/DataTable.tsx +34 -0
- package/src/components/panels/Diagram.tsx +67 -0
- package/src/components/panels/Feed.tsx +46 -0
- package/src/components/panels/Fork.tsx +86 -0
- package/src/components/panels/PanelCard.tsx +44 -0
- package/src/components/panels/Proof.tsx +64 -0
- package/src/components/panels/Stat.tsx +28 -0
- package/src/components/panels/Status.tsx +41 -0
- package/src/components/panels/StreamResume.tsx +48 -0
- package/src/components/panels/UploadProgress.tsx +42 -0
- package/src/components/panels/index.tsx +34 -0
- package/src/content/clarinet-3.18.beats.ts +58 -0
- package/src/content/gradient-demo.beats.ts +29 -0
- package/src/content/index-decoded.beats.ts +93 -0
- package/src/content/mainnet-launch.beats.ts +70 -0
- package/src/content/panels.beats.ts +40 -0
- package/src/content/sdk-6.5-mempool.beats.ts +67 -0
- package/src/content/streams-launch.beats.ts +143 -0
- package/src/content/streams.beats.ts +46 -0
- package/src/index.ts +4 -0
- package/src/motion/names.ts +29 -0
- package/src/motion/presets.ts +67 -0
- package/src/motion/useMotion.ts +63 -0
- package/src/prepare.ts +33 -0
- package/src/schema/beats.ts +161 -0
- package/src/templates/changelog-reel.ts +28 -0
- package/src/templates/feature-launch.ts +26 -0
- package/src/templates/index.ts +13 -0
- package/src/templates/milestone.ts +25 -0
- package/src/templates/parts.ts +32 -0
- package/src/templates/types.ts +31 -0
- package/src/theme/default.ts +39 -0
- package/src/theme/derive.ts +119 -0
- package/src/theme/index.ts +33 -0
- package/src/theme/library.ts +60 -0
- package/src/theme/slate.ts +39 -0
- package/src/theme/types.ts +67 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Waits
|
|
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 all
|
|
13
|
+
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 THE
|
|
21
|
+
SOFTWARE.
|
package/MOTION.md
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Motion vocabulary
|
|
2
|
+
|
|
3
|
+
The controlled lexicon every changelog beat speaks. Each name is implemented in
|
|
4
|
+
`src/motion/presets.ts`, frozen in `src/motion/names.ts`, and resolved by the
|
|
5
|
+
`useMotion` hook (`src/motion/useMotion.ts`). Beats reference these names as
|
|
6
|
+
strings — they never inline animation code.
|
|
7
|
+
|
|
8
|
+
## Principles (from the product DESIGN.md)
|
|
9
|
+
|
|
10
|
+
- **Ease-out only. No bounce, no elastic, never animate layout.** Entrances move
|
|
11
|
+
opacity + `transform`/`filter`, not width/height/top/left.
|
|
12
|
+
- **Two easings, two jobs.**
|
|
13
|
+
- `smooth` — `cubic-bezier(0.19, 1, 0.22, 1)`. Pure ease-out. **Every entrance.**
|
|
14
|
+
- `snappy` — `cubic-bezier(0.175, 0.885, 0.32, 1.1)`. Has a deliberate slight
|
|
15
|
+
settle/overshoot. **State-feedback pops only** (the NEW badge, a toggle) —
|
|
16
|
+
never a big entrance.
|
|
17
|
+
- **Respect reduced-motion.** Every preset has a `reduced` variant that drops to
|
|
18
|
+
opacity-only.
|
|
19
|
+
- **Signal Blue ≤10%, one Marker-Pink gesture per scene** (the `draw`n NEW badge).
|
|
20
|
+
|
|
21
|
+
## Enter presets
|
|
22
|
+
|
|
23
|
+
| Name | Motion | Use for |
|
|
24
|
+
|------|--------|---------|
|
|
25
|
+
| `rise` | translateY ↑ + fade | headlines, single cards |
|
|
26
|
+
| `settle` | scale 1.03→1 + fade | windows, data panels arriving |
|
|
27
|
+
| `bloom` | fade + blur 8px→0 | painting backgrounds |
|
|
28
|
+
| `type` | character-by-character typewriter | code, terminal lines |
|
|
29
|
+
| `stagger` | sequential `rise` of children | list / table rows |
|
|
30
|
+
| `draw` | SVG stroke reveal | diagrams, the NEW badge circle (Marker Pink) |
|
|
31
|
+
| `count` | numeric tween 0→value | metrics, counters |
|
|
32
|
+
|
|
33
|
+
`type` / `draw` / `count` carry their value internally — the preset gates
|
|
34
|
+
visibility, the helpers (`typewriterChars`, `drawDashoffset`, `countValue`,
|
|
35
|
+
`staggerDelay`) drive the value.
|
|
36
|
+
|
|
37
|
+
## Exit presets
|
|
38
|
+
|
|
39
|
+
| Name | Motion | Use for |
|
|
40
|
+
|------|--------|---------|
|
|
41
|
+
| `sink` | translateY ↑ + fade out | headlines leaving |
|
|
42
|
+
| `dissolve` | fade out | most panels / code |
|
|
43
|
+
| `lift` | scale 1→1.02 + fade out | a window departing forward |
|
|
44
|
+
| `cut` | instant | hard scene change |
|
|
45
|
+
|
|
46
|
+
## Style taxonomy — what each content type speaks
|
|
47
|
+
|
|
48
|
+
The default grammar a beat follows unless overridden. This is the "language":
|
|
49
|
+
pick the row, get the motion.
|
|
50
|
+
|
|
51
|
+
| Content | Enter | Exit | Easing | Notes |
|
|
52
|
+
|---------|-------|------|--------|-------|
|
|
53
|
+
| Headline | `rise` | `sink` | smooth | eyebrow = Caveat marker-pink, `draw`n in after |
|
|
54
|
+
| Eyebrow / NEW badge | `draw` | `dissolve` | smooth | the one human flourish; Marker Pink |
|
|
55
|
+
| Code window | `settle` + `type` | `dissolve` | smooth | caret blinks; typing is internal |
|
|
56
|
+
| Terminal line | `type` | `dissolve` | smooth | mono, single line |
|
|
57
|
+
| Data panel | `settle`, rows `stagger` | `dissolve` | smooth | "streaming…" pulse is ambient, not an entrance |
|
|
58
|
+
| Diagram | `draw` (edges) + `settle` (nodes) | `dissolve` | smooth | one filled accent node |
|
|
59
|
+
| Metric | `count` | `dissolve` | smooth | Fira Code, tabular-nums |
|
|
60
|
+
| Background | `bloom` + Ken Burns | `dissolve` | smooth | slow push-in over the whole beat |
|
|
61
|
+
|
|
62
|
+
## MotionSpec
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
{ enter?, exit?, easing?: "smooth" | "snappy", delay?, durationInFrames?, distance? }
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`delay` and `durationInFrames` are in frames; `distance` is px travel for `rise`.
|
|
69
|
+
`easing` defaults to `smooth`. See the live `MotionReel` composition for each
|
|
70
|
+
preset playing.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Cadence
|
|
2
|
+
|
|
3
|
+
**A changelog video engine that refuses to fabricate your product.**
|
|
4
|
+
|
|
5
|
+
Point it at a repo or a release and get an on-brand launch / changelog / announcement
|
|
6
|
+
video — real code from your project, your colors, rendered locally. No invented APIs,
|
|
7
|
+
no generic AI-video look, no hosted service. Free and open source.
|
|
8
|
+
|
|
9
|
+
> **Ask your agent for a launch video.** With the skill installed, say *"make a
|
|
10
|
+
> changelog video for this release"* — it reads your repo, writes the beats, and
|
|
11
|
+
> renders an MP4.
|
|
12
|
+
|
|
13
|
+

|
|
14
|
+

|
|
15
|
+
|
|
16
|
+
→ See the [gallery](docs/gallery.md) for the same engine across themes, formats, and arcs.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npx skills add ryanwaits/cadence # adds the skill to Claude Code / Cursor / Codex
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then just ask your agent. Or drive the CLI directly:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npx cadence create --release owner/name --install "npm i your-pkg" # a repo → a video
|
|
28
|
+
npx cadence create my.beats.json --format 9x16 # a beats file → a video
|
|
29
|
+
npx cadence themes # list built-in themes
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A render runs entirely on your machine (or your own GitHub Actions) — free, no API key.
|
|
33
|
+
|
|
34
|
+
## Why it doesn't look AI-generated
|
|
35
|
+
|
|
36
|
+
Most AI video either fabricates a fake product or reaches for the same neon-on-black
|
|
37
|
+
template. Cadence refuses both:
|
|
38
|
+
|
|
39
|
+
- **Refuses to invent API.** The agent climbs an honesty ladder — types → examples →
|
|
40
|
+
README → install-only — and never shows a snippet it can't verify in your repo.
|
|
41
|
+
- **Refuses a baked-in brand.** Colors and fonts come from *your* theme — derive one
|
|
42
|
+
from your URL (`cadence study --from-url`), a screenshot, or pick from 18 built-ins.
|
|
43
|
+
- **Refuses generic backdrops.** The default is a procedural, theme-colored field of
|
|
44
|
+
soft gradient arcs (no asset, no key); an optional pack offers 19th-century landscape
|
|
45
|
+
paintings instead of stock gradients.
|
|
46
|
+
- **Refuses freehand animation.** A controlled motion lexicon — ease-out only, no
|
|
47
|
+
bounce — so every video moves the same, considered way.
|
|
48
|
+
|
|
49
|
+
The whole surface is small and countable: **7 enter + 4 exit transitions, 9 panel
|
|
50
|
+
kinds, 3 formats, 18 themes — and one honesty rule: zero invented code.**
|
|
51
|
+
|
|
52
|
+
## How it works
|
|
53
|
+
|
|
54
|
+
A video is a list of **beats** — a small JSON file (`*.beats.json`). Each beat is a
|
|
55
|
+
generic unit: an optional background + headline + optional code window + optional
|
|
56
|
+
panel. The engine owns motion, fonts, layout, the typewriter, and code highlighting,
|
|
57
|
+
so the same engine makes changelogs, feature drops, launch announcements, and
|
|
58
|
+
milestones. The agent (via the skill) writes the beats; you tweak and render.
|
|
59
|
+
|
|
60
|
+
Panels visualize what a change *produces* — pick by result:
|
|
61
|
+
`feed · data-table · status · stat · proof · stream-resume · fork · upload-progress · diagram`.
|
|
62
|
+
|
|
63
|
+
- **[docs/recipes.md](docs/recipes.md)** — worked examples (changelog, announcement, showcase, brand-from-URL, CI).
|
|
64
|
+
- **[docs/gallery.md](docs/gallery.md)** — the same engine, visibly different videos.
|
|
65
|
+
- **[WALKTHROUGH.md](WALKTHROUGH.md)** — architecture + how to adjust each part.
|
|
66
|
+
- **[MOTION.md](MOTION.md)** — the motion lexicon + taxonomy.
|
|
67
|
+
|
|
68
|
+
## CLI
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
cadence create <repo|beats.json> build a video (repo flow, or a beats file)
|
|
72
|
+
cadence study --from-url <url> a brand URL / color → a theme JSON
|
|
73
|
+
cadence audit <beats.json> heuristic checks on a beats file (no auto-fix)
|
|
74
|
+
cadence redesign <beats.json> re-skin a beats file (new theme / background)
|
|
75
|
+
cadence themes | templates list built-ins
|
|
76
|
+
cadence guide interactive walkthrough
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Shared flags: `--format 16x9|1x1|9x16`, `--theme <name>`, `--theme-file <path>`,
|
|
80
|
+
`--frame <n>` (still preview).
|
|
81
|
+
|
|
82
|
+
## Ship on release (CI)
|
|
83
|
+
|
|
84
|
+
The included GitHub Action renders a video on every release, free, on your own
|
|
85
|
+
runner — no hosted service. See **[docs/github-action.md](docs/github-action.md)**.
|
|
86
|
+
|
|
87
|
+
## Develop the engine
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
bun install
|
|
91
|
+
bun run dev # Remotion Studio
|
|
92
|
+
bun run check # tsc
|
|
93
|
+
bun run check:motion # MOTION.md ↔ presets parity
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Layout: `src/schema` (the beats contract), `src/components` (scene + panels),
|
|
97
|
+
`src/motion` (the lexicon), `src/theme` (themes + derivation), `src/templates`
|
|
98
|
+
(the no-LLM repo→beats path), `prompts/art` (the optional painterly pack).
|
|
99
|
+
|
|
100
|
+
## Optional: painterly backgrounds
|
|
101
|
+
|
|
102
|
+
The default backdrop is procedural and needs no key. For the optional
|
|
103
|
+
"Hill Country Sublime" landscape pack (see **[ART-DIRECTION.md](ART-DIRECTION.md)**):
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
cadence art --landmark pennybacker --level heightened # needs OPENAI_API_KEY
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
This is the only part of the toolchain that calls an external API.
|
package/bin/cli.mjs
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* npm bin launcher. node can't execute TypeScript directly, so this resolves
|
|
4
|
+
* the `tsx` runner (package-local `.bin`, then the hoisted top-level `.bin`
|
|
5
|
+
* when we're installed under `node_modules/<pkg>`, else PATH) and runs the
|
|
6
|
+
* TS CLI. Mirrors `scripts/_pkg.ts#binPath` — kept in JS so the bin needs no
|
|
7
|
+
* transpile step.
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { dirname, join, resolve } from "node:path";
|
|
12
|
+
import { fileURLToPath } from "node:url";
|
|
13
|
+
|
|
14
|
+
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
15
|
+
const tsx =
|
|
16
|
+
[join(root, "node_modules/.bin/tsx"), resolve(root, "../.bin/tsx")].find(existsSync) ?? "tsx";
|
|
17
|
+
|
|
18
|
+
const res = spawnSync(tsx, [join(root, "scripts/cli.ts"), ...process.argv.slice(2)], {
|
|
19
|
+
stdio: "inherit",
|
|
20
|
+
});
|
|
21
|
+
process.exit(res.status ?? 0);
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@waits/cadence",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"bin": {
|
|
7
|
+
"cadence": "bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/ryanwaits/cadence.git"
|
|
12
|
+
},
|
|
13
|
+
"files": [
|
|
14
|
+
"bin",
|
|
15
|
+
"scripts",
|
|
16
|
+
"src",
|
|
17
|
+
"prompts/art",
|
|
18
|
+
"public/backgrounds/*.png",
|
|
19
|
+
"MOTION.md",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"scripts": {
|
|
24
|
+
"cli": "tsx scripts/cli.ts",
|
|
25
|
+
"guide": "tsx scripts/guide.ts",
|
|
26
|
+
"dev": "remotion studio",
|
|
27
|
+
"render": "tsx scripts/render.ts",
|
|
28
|
+
"art": "tsx scripts/generate-art.ts",
|
|
29
|
+
"changes": "tsx scripts/changes.ts",
|
|
30
|
+
"create": "tsx scripts/make.ts",
|
|
31
|
+
"study": "tsx scripts/theme.ts",
|
|
32
|
+
"audit": "tsx scripts/audit.ts",
|
|
33
|
+
"redesign": "tsx scripts/redesign.ts",
|
|
34
|
+
"sync-skill": "tsx scripts/sync-skill.ts",
|
|
35
|
+
"still": "remotion still",
|
|
36
|
+
"check": "tsc --noEmit",
|
|
37
|
+
"check:motion": "tsx scripts/check-motion.ts",
|
|
38
|
+
"check:render": "tsx scripts/smoke.ts",
|
|
39
|
+
"changeset": "changeset",
|
|
40
|
+
"version": "changeset version",
|
|
41
|
+
"release": "changeset publish"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"@remotion/cli": "4.0.471",
|
|
45
|
+
"@remotion/google-fonts": "4.0.471",
|
|
46
|
+
"react": "19.0.0",
|
|
47
|
+
"react-dom": "19.0.0",
|
|
48
|
+
"remotion": "4.0.471",
|
|
49
|
+
"shiki": "4.2.0",
|
|
50
|
+
"tsx": "4.22.4",
|
|
51
|
+
"zod": "4.4.3"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@changesets/changelog-git": "^0.2.1",
|
|
55
|
+
"@changesets/cli": "^2.31.0",
|
|
56
|
+
"@types/react": "19.2.16",
|
|
57
|
+
"typescript": "5.7.3"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Format } from "../../src/schema/beats";
|
|
2
|
+
import { COMPOSITION, NEGATIVES, STYLE } from "./style";
|
|
3
|
+
import { LANDMARKS, type LandmarkKey } from "./landmarks";
|
|
4
|
+
import { FANTASY, type FantasyLevel } from "./fantasy";
|
|
5
|
+
|
|
6
|
+
/** Build the full image-gen prompt for a landmark × fantasy level × format. */
|
|
7
|
+
export function composePrompt(landmark: LandmarkKey, level: FantasyLevel, format: Format): string {
|
|
8
|
+
return [
|
|
9
|
+
STYLE,
|
|
10
|
+
`Subject: ${LANDMARKS[landmark].subject}.`,
|
|
11
|
+
FANTASY[level],
|
|
12
|
+
COMPOSITION[format],
|
|
13
|
+
NEGATIVES,
|
|
14
|
+
].join(" ");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { LANDMARKS, FANTASY };
|
|
18
|
+
export type { LandmarkKey, FantasyLevel };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fantasy dial. Grounded keeps it plausibly Texan; Heightened is the default
|
|
3
|
+
* launch register (recognizable but sublime); Mythic goes full Bierstadt fever-
|
|
4
|
+
* dream. Same landmark, escalating wilderness around it.
|
|
5
|
+
*/
|
|
6
|
+
export const FANTASY = {
|
|
7
|
+
grounded:
|
|
8
|
+
"Keep the landmark and Texas Hill Country terrain recognizable and naturalistic — only the light is idealized: a glowing, atmospheric golden hour.",
|
|
9
|
+
heightened:
|
|
10
|
+
"Heighten the drama: exaggerate the scale of the cliffs and sky, widen the river into a grand basin, add distant snow-dusted Southwest peaks and deeper atmospheric haze — recognizable but sublime.",
|
|
11
|
+
mythic:
|
|
12
|
+
"Full sublime fantasy: impossible towering mesas and spires, a vast alpine-scale lake, layered floating mist, cathedral shafts of light — a dreamlike, mythic grandeur while the landmark remains just identifiable.",
|
|
13
|
+
} as const;
|
|
14
|
+
|
|
15
|
+
export type FantasyLevel = keyof typeof FANTASY;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Austin / Texas Hill Country / Southwest landmarks, each described as a subject
|
|
3
|
+
* clause to drop into the luminist style. The recognizable landmark grounds the
|
|
4
|
+
* fantasy; the `fantasy` dial then exaggerates the wilderness around it.
|
|
5
|
+
*/
|
|
6
|
+
export const LANDMARKS = {
|
|
7
|
+
pennybacker: {
|
|
8
|
+
name: "Pennybacker (360) Bridge",
|
|
9
|
+
subject:
|
|
10
|
+
"the graceful rust-red Pennybacker 360 steel arch bridge spanning a deep Hill Country river gorge, cedar-covered limestone cliffs rising on either side above a wide still river",
|
|
11
|
+
},
|
|
12
|
+
utTower: {
|
|
13
|
+
name: "UT Tower",
|
|
14
|
+
subject:
|
|
15
|
+
"a tall pale limestone bell tower in the manner of the University of Texas tower crowning a forested hill, glowing in golden light above a valley",
|
|
16
|
+
},
|
|
17
|
+
capitol: {
|
|
18
|
+
name: "Texas Capitol",
|
|
19
|
+
subject:
|
|
20
|
+
"a great domed sandstone capitol building in the manner of the Texas State Capitol, its pink-granite dome catching the light atop a broad rise of live oaks",
|
|
21
|
+
},
|
|
22
|
+
congress: {
|
|
23
|
+
name: "Congress Ave Bridge",
|
|
24
|
+
subject:
|
|
25
|
+
"a long low arched stone bridge over a wide calm urban lake at dusk in the manner of the Congress Avenue bridge, a faint cloud of bats rising in the distance",
|
|
26
|
+
},
|
|
27
|
+
mountBonnell: {
|
|
28
|
+
name: "Mount Bonnell",
|
|
29
|
+
subject:
|
|
30
|
+
"a high limestone overlook in the manner of Mount Bonnell above a winding emerald river far below, cedar and prickly-pear clinging to the rocky bluff",
|
|
31
|
+
},
|
|
32
|
+
enchantedRock: {
|
|
33
|
+
name: "Enchanted Rock",
|
|
34
|
+
subject:
|
|
35
|
+
"an enormous rounded pink-granite dome in the manner of Enchanted Rock swelling from the Hill Country scrub, monumental and weathered",
|
|
36
|
+
},
|
|
37
|
+
hamiltonPool: {
|
|
38
|
+
name: "Hamilton Pool",
|
|
39
|
+
subject:
|
|
40
|
+
"a collapsed-grotto jade-green grotto pool in the manner of Hamilton Pool, a thin waterfall spilling from a moss-hung limestone overhang into still water",
|
|
41
|
+
},
|
|
42
|
+
bartonSprings: {
|
|
43
|
+
name: "Barton Springs",
|
|
44
|
+
subject:
|
|
45
|
+
"a long spring-fed natural pool of clear turquoise water in the manner of Barton Springs, lined with bald cypress and pecan, fed by limestone springs",
|
|
46
|
+
},
|
|
47
|
+
} as const;
|
|
48
|
+
|
|
49
|
+
export type LandmarkKey = keyof typeof LANDMARKS;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The luminist DNA preamble — constant across every "Hill Country Sublime"
|
|
3
|
+
* painting. Encodes the Hudson River School lineage, the light, and the palette,
|
|
4
|
+
* reconciled with the Field Notebook brand so paper/ink/Signal-Blue UI reads
|
|
5
|
+
* cleanly on top. See ART-DIRECTION.md for the full rationale.
|
|
6
|
+
*/
|
|
7
|
+
export const STYLE = `A grand 19th-century Luminist landscape oil painting in the Hudson River School tradition of Albert Bierstadt, Thomas Moran, and Frederic Edwin Church. Soft painterly brushwork, deep atmospheric perspective, glowing diffuse light with a single cool source low in a clear sky, drifting haze and god-rays breaking through luminous towering clouds. Museum-quality, serene, monumental, contemplative. Palette of slate blue, steel and cerulean, cool limestone grey-white, sage and cedar green, pale periwinkle, and a luminous cobalt-to-cream sky echoing a signal blue (#2563eb) — cool, airy, and atmospheric, never garish.`;
|
|
8
|
+
|
|
9
|
+
/** Composition guidance so the painting works as a backdrop for the UI layer. */
|
|
10
|
+
export const COMPOSITION = {
|
|
11
|
+
"16x9":
|
|
12
|
+
"Compose for a wide 16:9 frame: keep the upper third a calm, uncluttered luminous sky and the lower third a still, mirror-like water plane — these are intentional areas of negative space for overlaid text and UI. The focal landform sits in the middle distance, slightly off-center.",
|
|
13
|
+
"1x1":
|
|
14
|
+
"Compose for a square 1:1 frame: generous calm sky filling the top half, a still water plane across the bottom, the focal landform centered in the middle distance. Leave the upper-center quiet for overlaid text.",
|
|
15
|
+
"9x16":
|
|
16
|
+
"Compose for a tall vertical 9:16 frame: a soaring luminous sky in the upper half, the focal landform in the middle band, and a still reflective water plane in the lower third. Keep the vertical center column calm for stacked UI overlays.",
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
/** Always appended — keeps the frame clean for compositing. */
|
|
20
|
+
export const NEGATIVES = "No text, no lettering, no watermark, no people in the foreground, no modern vehicles, no signage, no harsh saturated colors.";
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/scripts/_pkg.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution that works both in-repo (dev) and from an installed npm
|
|
3
|
+
* package. The engine's own files (the Remotion entry, sibling scripts, dep
|
|
4
|
+
* bins) must resolve relative to *this package*, not the user's CWD — only
|
|
5
|
+
* user-supplied paths (a beats file, the `out/` dir) are CWD-relative.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { dirname, join, resolve } from "node:path";
|
|
9
|
+
import { fileURLToPath } from "node:url";
|
|
10
|
+
|
|
11
|
+
/** Absolute path to the engine package root (one level above `scripts/`). */
|
|
12
|
+
export const PKG_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
13
|
+
|
|
14
|
+
/** Absolute path to one of the engine's own files (e.g. `src/index.ts`). */
|
|
15
|
+
export const pkgFile = (rel: string) => join(PKG_ROOT, rel);
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a dependency's CLI bin, robust to install layout: package-local
|
|
19
|
+
* `.bin` first (dev + nested installs), then the hoisted top-level `.bin`
|
|
20
|
+
* (our package living under `node_modules/<pkg>`), else assume it's on PATH.
|
|
21
|
+
*/
|
|
22
|
+
export function binPath(name: string): string {
|
|
23
|
+
const local = join(PKG_ROOT, "node_modules", ".bin", name);
|
|
24
|
+
if (existsSync(local)) return local;
|
|
25
|
+
const hoisted = resolve(PKG_ROOT, "..", ".bin", name);
|
|
26
|
+
if (existsSync(hoisted)) return hoisted;
|
|
27
|
+
return name;
|
|
28
|
+
}
|
package/scripts/audit.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cadence audit <beats>` — static, heuristic checks on a beats file. Advisory
|
|
3
|
+
* only: ranked findings, no auto-fix (edit content by hand; use `redesign` to
|
|
4
|
+
* re-skin). Mirrors the authoring guidance in the cadence skill.
|
|
5
|
+
*
|
|
6
|
+
* cadence audit src/content/streams-launch.beats.ts
|
|
7
|
+
*/
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { pathToFileURL } from "node:url";
|
|
11
|
+
import { changelogSchema } from "../src/schema/beats";
|
|
12
|
+
|
|
13
|
+
const file = process.argv.slice(2).find((a) => !a.startsWith("-"));
|
|
14
|
+
if (!file) {
|
|
15
|
+
console.error("usage: cadence audit <beats.ts|.json>");
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const raw = file.endsWith(".json")
|
|
20
|
+
? JSON.parse(readFileSync(resolve(file), "utf8"))
|
|
21
|
+
: (await import(pathToFileURL(resolve(file)).href)).default;
|
|
22
|
+
const parsed = changelogSchema.parse(raw);
|
|
23
|
+
const beats = parsed.beats;
|
|
24
|
+
const FPS = 30;
|
|
25
|
+
|
|
26
|
+
type Level = "error" | "warn" | "info";
|
|
27
|
+
const findings: { level: Level; msg: string }[] = [];
|
|
28
|
+
const add = (level: Level, msg: string) => findings.push({ level, msg });
|
|
29
|
+
|
|
30
|
+
// 1. Beat count — skill guidance is to keep it tight (3-6).
|
|
31
|
+
if (beats.length < 2) add("warn", `only ${beats.length} beat — most videos want 3-6`);
|
|
32
|
+
if (beats.length > 7) add("warn", `${beats.length} beats — tighten toward 3-6; long videos lose attention`);
|
|
33
|
+
|
|
34
|
+
// 2. Per-beat duration (≈2-10s at 30fps).
|
|
35
|
+
beats.forEach((b, i) => {
|
|
36
|
+
const s = (b.durationInFrames / FPS).toFixed(1);
|
|
37
|
+
if (b.durationInFrames < 45) add("warn", `beat ${i + 1} "${b.headline}" is ${s}s — too short to read`);
|
|
38
|
+
else if (b.durationInFrames > 360) add("warn", `beat ${i + 1} "${b.headline}" is ${s}s — likely too long`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// 3. Headline length — short + declarative.
|
|
42
|
+
beats.forEach((b, i) => {
|
|
43
|
+
if (b.headline.length > 48)
|
|
44
|
+
add("warn", `beat ${i + 1} headline is ${b.headline.length} chars — shorten ("${b.headline.slice(0, 40)}…")`);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 4. Motion monotony — every beat with an explicit enter uses the same one.
|
|
48
|
+
const enters = beats.map((b) => b.headlineMotion?.enter).filter(Boolean);
|
|
49
|
+
if (enters.length >= 3 && new Set(enters).size === 1)
|
|
50
|
+
add("info", `every headline uses the "${enters[0]}" enter — vary it for rhythm`);
|
|
51
|
+
|
|
52
|
+
// 5. Install/CTA closer — heuristic: last beat centered with a bash command or a badge/caption.
|
|
53
|
+
const last = beats[beats.length - 1];
|
|
54
|
+
const isCloser = last.layout === "center" && (last.code?.lang === "bash" || !!last.badge || !!last.caption);
|
|
55
|
+
if (!isCloser) add("info", "last beat doesn't look like an install/CTA closer (centered + install command)");
|
|
56
|
+
|
|
57
|
+
// Report, ranked: error → warn → info. "Issues" = error+warn; info are notes.
|
|
58
|
+
const order: Record<Level, number> = { error: 0, warn: 1, info: 2 };
|
|
59
|
+
findings.sort((a, b) => order[a.level] - order[b.level]);
|
|
60
|
+
const icon: Record<Level, string> = { error: "✗", warn: "▲", info: "·" };
|
|
61
|
+
const issues = findings.filter((f) => f.level !== "info").length;
|
|
62
|
+
|
|
63
|
+
if (issues === 0) {
|
|
64
|
+
const notes = findings.length ? ` (${findings.length} note${findings.length > 1 ? "s" : ""} below)` : "";
|
|
65
|
+
console.log(`✓ ${file}: no issues across ${beats.length} beats${notes}`);
|
|
66
|
+
} else {
|
|
67
|
+
console.log(`${file}: ${issues} issue${issues > 1 ? "s" : ""} across ${beats.length} beats`);
|
|
68
|
+
}
|
|
69
|
+
for (const f of findings) console.log(` ${icon[f.level]} ${f.msg}`);
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read what changed in a repo → a normalized UpdateManifest (JSON on stdout).
|
|
3
|
+
* The deterministic "reads your repo" half of the pipeline; the brain/template
|
|
4
|
+
* turns the manifest into beats.
|
|
5
|
+
*
|
|
6
|
+
* tsx scripts/changes.ts --release stx-labs/clarinet [--tag v3.18.0] [--install "brew install clarinet"]
|
|
7
|
+
* tsx scripts/changes.ts --changelog ./CHANGELOG.md --product my-pkg --install "npm i my-pkg"
|
|
8
|
+
*/
|
|
9
|
+
import { spawnSync } from "node:child_process";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { basename, dirname, join } from "node:path";
|
|
12
|
+
import { manifestFromChangelogText, manifestFromReleaseBody } from "../src/adapters";
|
|
13
|
+
|
|
14
|
+
/** Warn (don't fail) if a sibling package.json version disagrees with the changelog's. */
|
|
15
|
+
function warnIfStale(changelogPath: string, changelogVersion: string) {
|
|
16
|
+
if (!changelogVersion) return;
|
|
17
|
+
try {
|
|
18
|
+
const pkgPath = join(dirname(changelogPath), "package.json");
|
|
19
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
20
|
+
const pkgVersion = String(pkg.version ?? "").replace(/^v/i, "").trim();
|
|
21
|
+
if (pkgVersion && pkgVersion !== changelogVersion) {
|
|
22
|
+
console.error(
|
|
23
|
+
`warning: ${pkgPath} is at ${pkgVersion} but the changelog's top entry is ${changelogVersion} — the changelog may be stale.`
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
} catch {
|
|
27
|
+
// no sibling package.json / unreadable — nothing to compare against
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const args = process.argv.slice(2);
|
|
32
|
+
const flag = (n: string) => {
|
|
33
|
+
const eq = args.find((a) => a.startsWith(`${n}=`));
|
|
34
|
+
if (eq) return eq.split("=")[1];
|
|
35
|
+
const i = args.indexOf(n);
|
|
36
|
+
return i >= 0 && !args[i + 1]?.startsWith("--") ? args[i + 1] : undefined;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const release = flag("--release"); // owner/name
|
|
40
|
+
const changelog = flag("--changelog");
|
|
41
|
+
const install = flag("--install");
|
|
42
|
+
|
|
43
|
+
let manifest;
|
|
44
|
+
if (release) {
|
|
45
|
+
const tag = flag("--tag");
|
|
46
|
+
const gh = ["release", "view", ...(tag ? [tag] : []), "--repo", release, "--json", "tagName,name,publishedAt,body"];
|
|
47
|
+
const res = spawnSync("gh", gh, { encoding: "utf8" });
|
|
48
|
+
if (res.status !== 0) {
|
|
49
|
+
console.error(`gh release view failed for ${release}:\n${res.stderr}`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
const r = JSON.parse(res.stdout);
|
|
53
|
+
manifest = manifestFromReleaseBody({
|
|
54
|
+
product: flag("--product") ?? basename(release),
|
|
55
|
+
version: r.tagName ?? r.name ?? "",
|
|
56
|
+
body: r.body ?? "",
|
|
57
|
+
date: r.publishedAt?.slice(0, 10),
|
|
58
|
+
install,
|
|
59
|
+
repoUrl: `https://github.com/${release}`,
|
|
60
|
+
});
|
|
61
|
+
} else if (changelog) {
|
|
62
|
+
const text = readFileSync(changelog, "utf8");
|
|
63
|
+
manifest = manifestFromChangelogText(text, { product: flag("--product") ?? "package", install });
|
|
64
|
+
warnIfStale(changelog, manifest.version);
|
|
65
|
+
} else {
|
|
66
|
+
console.error("usage: changes.ts --release <owner/name> [--tag vX] | --changelog <path> [--product X] [--install '...']");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log(JSON.stringify(manifest, null, 2));
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parity guard: every preset in names.ts must be documented in MOTION.md, and
|
|
3
|
+
* every preset named in MOTION.md's Enter/Exit tables must exist in names.ts.
|
|
4
|
+
* Run: `bun run check:motion` (or `tsx scripts/check-motion.ts`).
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync } from "node:fs";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import { dirname, join } from "node:path";
|
|
9
|
+
import { ENTER_PRESETS, EXIT_PRESETS } from "../src/motion/names";
|
|
10
|
+
|
|
11
|
+
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
|
+
const doc = readFileSync(join(root, "MOTION.md"), "utf8");
|
|
13
|
+
const all = [...ENTER_PRESETS, ...EXIT_PRESETS];
|
|
14
|
+
const errors: string[] = [];
|
|
15
|
+
|
|
16
|
+
// forward: each name appears as `name` in the doc
|
|
17
|
+
for (const name of all) {
|
|
18
|
+
if (!doc.includes(`\`${name}\``)) errors.push(`MOTION.md is missing preset \`${name}\``);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// backward: first-column backticked names in the two preset tables exist in names.ts
|
|
22
|
+
const tableNames = new Set<string>();
|
|
23
|
+
for (const section of ["## Enter presets", "## Exit presets"]) {
|
|
24
|
+
const start = doc.indexOf(section);
|
|
25
|
+
if (start === -1) { errors.push(`MOTION.md missing section "${section}"`); continue; }
|
|
26
|
+
const body = doc.slice(start, doc.indexOf("\n## ", start + 1) === -1 ? undefined : doc.indexOf("\n## ", start + 1));
|
|
27
|
+
for (const m of body.matchAll(/^\|\s*`([a-z]+)`\s*\|/gm)) tableNames.add(m[1]);
|
|
28
|
+
}
|
|
29
|
+
for (const name of tableNames) {
|
|
30
|
+
if (!(all as string[]).includes(name)) errors.push(`MOTION.md documents unknown preset \`${name}\``);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (errors.length) {
|
|
34
|
+
console.error("✗ motion parity failed:\n" + errors.map((e) => " - " + e).join("\n"));
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
console.log(`✓ motion parity: ${all.length} presets documented (${tableNames.size} in tables)`);
|