@anvpx/motion-build 0.0.0-stage → 0.1.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.
Files changed (145) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +67 -2
  3. package/dist/.out/demo/boot.js +90 -0
  4. package/dist/.out/demo/runtime/easing.js +45 -0
  5. package/dist/.out/demo/runtime/mount.js +153 -0
  6. package/dist/.out/demo/runtime/timeline.js +199 -0
  7. package/dist/.out/demo/scenes/count/scene.js +6 -0
  8. package/dist/.out/demo/scenes/intro/scene.js +6 -0
  9. package/dist/.out/demo/scenes/outro/scene.js +6 -0
  10. package/dist/.out/demo/scenes/path/scene.js +6 -0
  11. package/dist/.out/demo/scenes/signals/scene.js +6 -0
  12. package/dist/.out/demo/shared/format.js +59 -0
  13. package/dist/.out/demo/shared/motion.js +117 -0
  14. package/dist/.out/demo-overlays/boot.js +95 -0
  15. package/dist/.out/demo-overlays/runtime/easing.js +45 -0
  16. package/dist/.out/demo-overlays/runtime/mount.js +153 -0
  17. package/dist/.out/demo-overlays/runtime/timeline.js +199 -0
  18. package/dist/.out/demo-overlays/scenes/chapter-signals/scene.js +6 -0
  19. package/dist/.out/demo-overlays/scenes/count/scene.js +6 -0
  20. package/dist/.out/demo-overlays/scenes/flow-progress/scene.js +17 -0
  21. package/dist/.out/demo-overlays/scenes/intro/scene.js +6 -0
  22. package/dist/.out/demo-overlays/scenes/mark/scene.js +6 -0
  23. package/dist/.out/demo-overlays/scenes/outro/scene.js +6 -0
  24. package/dist/.out/demo-overlays/scenes/path/scene.js +6 -0
  25. package/dist/.out/demo-overlays/scenes/presenter/scene.js +6 -0
  26. package/dist/.out/demo-overlays/scenes/signals/scene.js +6 -0
  27. package/dist/.out/demo-overlays/scenes/tick/scene.js +21 -0
  28. package/dist/.out/demo-overlays/shared/format.js +59 -0
  29. package/dist/.out/demo-overlays/shared/motion.js +117 -0
  30. package/dist/assets.d.ts +36 -0
  31. package/dist/assets.js +95 -0
  32. package/dist/audio/approval.d.ts +45 -0
  33. package/dist/audio/approval.js +82 -0
  34. package/dist/audio/mix.d.ts +231 -0
  35. package/dist/audio/mix.js +491 -0
  36. package/dist/audio/music.d.ts +82 -0
  37. package/dist/audio/music.js +134 -0
  38. package/dist/audio/preview.d.ts +31 -0
  39. package/dist/audio/preview.js +31 -0
  40. package/dist/build.d.ts +65 -0
  41. package/dist/build.js +260 -0
  42. package/dist/cache.d.ts +59 -0
  43. package/dist/cache.js +249 -0
  44. package/dist/emit/boot.d.ts +16 -0
  45. package/dist/emit/boot.js +32 -0
  46. package/dist/emit/boot.template.js +85 -0
  47. package/dist/emit/fragment-guard.d.ts +5 -0
  48. package/dist/emit/fragment-guard.js +15 -0
  49. package/dist/emit/index-html.d.ts +26 -0
  50. package/dist/emit/index-html.js +63 -0
  51. package/dist/emit/safe-json.d.ts +2 -0
  52. package/dist/emit/safe-json.js +7 -0
  53. package/dist/emit/vendor.d.ts +7 -0
  54. package/dist/emit/vendor.js +57 -0
  55. package/dist/fonts.d.ts +39 -0
  56. package/dist/fonts.js +112 -0
  57. package/dist/index.d.ts +19 -0
  58. package/dist/index.js +19 -0
  59. package/dist/integration/render.integration.d.ts +1 -0
  60. package/dist/integration/render.integration.js +162 -0
  61. package/dist/lint/custom-source.d.ts +12 -0
  62. package/dist/lint/custom-source.js +130 -0
  63. package/dist/lint/fixtures.d.ts +24 -0
  64. package/dist/lint/fixtures.js +42 -0
  65. package/dist/lint/flow.d.ts +12 -0
  66. package/dist/lint/flow.js +38 -0
  67. package/dist/lint/frame.d.ts +3 -0
  68. package/dist/lint/frame.js +2 -0
  69. package/dist/lint/hook-first.d.ts +6 -0
  70. package/dist/lint/hook-first.js +27 -0
  71. package/dist/lint/index.d.ts +25 -0
  72. package/dist/lint/index.js +55 -0
  73. package/dist/lint/layout-estimate.d.ts +18 -0
  74. package/dist/lint/layout-estimate.js +165 -0
  75. package/dist/lint/linear-ease.d.ts +2 -0
  76. package/dist/lint/linear-ease.js +14 -0
  77. package/dist/lint/long-hold.d.ts +9 -0
  78. package/dist/lint/long-hold.js +52 -0
  79. package/dist/lint/min-hold.d.ts +4 -0
  80. package/dist/lint/min-hold.js +10 -0
  81. package/dist/lint/motion-craft.d.ts +18 -0
  82. package/dist/lint/motion-craft.js +112 -0
  83. package/dist/lint/narration-pace.d.ts +6 -0
  84. package/dist/lint/narration-pace.js +35 -0
  85. package/dist/lint/overlays.d.ts +9 -0
  86. package/dist/lint/overlays.js +64 -0
  87. package/dist/lint/reading-time.d.ts +5 -0
  88. package/dist/lint/reading-time.js +17 -0
  89. package/dist/lint/safe-zone-9x16.d.ts +11 -0
  90. package/dist/lint/safe-zone-9x16.js +101 -0
  91. package/dist/lint/text-floor.d.ts +8 -0
  92. package/dist/lint/text-floor.js +34 -0
  93. package/dist/lint/transition-implemented.d.ts +3 -0
  94. package/dist/lint/transition-implemented.js +15 -0
  95. package/dist/lint/transition-length.d.ts +4 -0
  96. package/dist/lint/transition-length.js +10 -0
  97. package/dist/lint/type-grid.d.ts +30 -0
  98. package/dist/lint/type-grid.js +147 -0
  99. package/dist/lint/types.d.ts +41 -0
  100. package/dist/lint/types.js +1 -0
  101. package/dist/lint/words-on-screen.d.ts +5 -0
  102. package/dist/lint/words-on-screen.js +24 -0
  103. package/dist/manifest.d.ts +53 -0
  104. package/dist/manifest.js +55 -0
  105. package/dist/render-manifest.d.ts +21 -0
  106. package/dist/render-manifest.js +7 -0
  107. package/dist/review/review.d.ts +15 -0
  108. package/dist/review/review.js +56 -0
  109. package/dist/templates/discover-check.d.ts +11 -0
  110. package/dist/templates/discover-check.js +36 -0
  111. package/dist/templates/discover.d.ts +37 -0
  112. package/dist/templates/discover.js +63 -0
  113. package/dist/templates/expand.d.ts +17 -0
  114. package/dist/templates/expand.js +184 -0
  115. package/dist/templates/fixtures.d.ts +4 -0
  116. package/dist/templates/fixtures.js +63 -0
  117. package/dist/templates/index.d.ts +8 -0
  118. package/dist/templates/index.js +6 -0
  119. package/dist/templates/instantiate.d.ts +43 -0
  120. package/dist/templates/instantiate.js +160 -0
  121. package/dist/templates/load.d.ts +16 -0
  122. package/dist/templates/load.js +122 -0
  123. package/dist/templates/pacing.d.ts +21 -0
  124. package/dist/templates/pacing.js +61 -0
  125. package/dist/templates/search.d.ts +64 -0
  126. package/dist/templates/search.js +129 -0
  127. package/dist/templates/semver.d.ts +2 -0
  128. package/dist/templates/semver.js +12 -0
  129. package/dist/templates/slots.d.ts +8 -0
  130. package/dist/templates/slots.js +116 -0
  131. package/dist/templates/style.d.ts +25 -0
  132. package/dist/templates/style.js +56 -0
  133. package/dist/templates/types.d.ts +29 -0
  134. package/dist/templates/types.js +1 -0
  135. package/dist/templates/validate.d.ts +17 -0
  136. package/dist/templates/validate.js +203 -0
  137. package/dist/timing.d.ts +38 -0
  138. package/dist/timing.js +88 -0
  139. package/dist/validate-full.d.ts +3 -0
  140. package/dist/validate-full.js +132 -0
  141. package/dist/verify.d.ts +32 -0
  142. package/dist/verify.js +180 -0
  143. package/dist/versions.d.ts +26 -0
  144. package/dist/versions.js +74 -0
  145. package/package.json +40 -3
@@ -0,0 +1,231 @@
1
+ import { type TimingManifest } from "@anvpx/motion-schema";
2
+ import { type MusicIdentity, type SeamlessLoop } from "./music.ts";
3
+ /** Sibling directory of a voice manifest holding `<scene-id>.wav` files. */
4
+ export declare const AUDIO_DIR_NAME = "audio";
5
+ /** One narration clip: absolute start plus its wav on disk. */
6
+ export interface MixClip {
7
+ readonly id: string;
8
+ readonly start: number;
9
+ readonly duration: number;
10
+ readonly wav: string;
11
+ }
12
+ /** Opt-in build input: which manifest to mix and where its wavs live. */
13
+ export interface AudioManifestRef {
14
+ readonly manifest: TimingManifest;
15
+ readonly audioDir: string;
16
+ /** Opt-in user CC0 bed; absent means narration-only (never a bundled fallback). */
17
+ readonly music?: MusicBed;
18
+ }
19
+ export type AudioManifestResult = {
20
+ ok: true;
21
+ manifest: TimingManifest;
22
+ } | {
23
+ ok: false;
24
+ problems: string[];
25
+ };
26
+ /** Wav path for one scene id inside the manifest's audio directory. */
27
+ export declare const wavPathFor: (audioDir: string, id: string) => string;
28
+ /** Manifest scenes in order, each resolved to its wav path. */
29
+ export declare const clipsFromManifest: (manifest: TimingManifest, audioDir: string) => MixClip[];
30
+ /** Clips whose wav is absent; non-empty means refuse to mix (fail closed). */
31
+ export declare const missingClips: (clips: readonly MixClip[], exists: (path: string) => boolean) => MixClip[];
32
+ /** Whole seconds to integer ffmpeg milliseconds; refuses nonsense. */
33
+ export declare const toDelayMs: (seconds: number) => number;
34
+ /** True when every scene starts where the previous one ends (voice norm). */
35
+ export declare const isContiguous: (scenes: readonly {
36
+ start: number;
37
+ duration: number;
38
+ }[]) => boolean;
39
+ /**
40
+ * Deterministic `-filter_complex` graph mixing the clips onto `[aout]`.
41
+ * `adelay` takes `ms:all=1` so mono and stereo wavs delay every channel alike;
42
+ * `amix` runs `normalize=0` because narrated scenes never overlap, so levels
43
+ * stay exactly as synthesized instead of being divided by the input count.
44
+ */
45
+ export declare const buildFilterGraph: (clips: readonly MixClip[]) => string;
46
+ /**
47
+ * Deterministic ffmpeg argv: silent video plus one `-i` per wav, video passed
48
+ * through (`-c:v copy`), narration mixed to aac, `-shortest` trimming any
49
+ * sub-frame tail so the output never outruns the picture.
50
+ */
51
+ export declare const buildMixArgs: (videoIn: string, clips: readonly MixClip[], out: string) => string[];
52
+ /** Validates an unknown document as a mixable TimingManifest (fail closed). */
53
+ export declare const parseAudioManifest: (value: unknown) => AudioManifestResult;
54
+ /** Opt-in user CC0 music bed: explicit wav plus ducked level (narration stays ahead). */
55
+ export interface MusicBed {
56
+ readonly wav: string;
57
+ readonly volume?: number;
58
+ }
59
+ export type { MusicIdentity, SeamlessLoop } from "./music.ts";
60
+ /**
61
+ * Ducked bed level, re-exported from the pinned music single source
62
+ * (`./music.ts`, itself from the schema); narration stays intelligible
63
+ * over the bed at the qc-lufs pinned depth of -6.0206 dB.
64
+ */
65
+ export { DEFAULT_MUSIC_VOLUME } from "./music.ts";
66
+ /** Resolve the bed level through the schema guard; anything else fails closed. */
67
+ export declare const resolveMusicVolume: (music: MusicBed) => number;
68
+ /**
69
+ * Bed wavs that are absent; non-empty means refuse to mix (fail closed).
70
+ * An absent bed (`undefined`) means narration-only — never a silent fallback.
71
+ */
72
+ export declare const missingMusic: (music: MusicBed | undefined, exists: (path: string) => boolean) => string[];
73
+ /**
74
+ * Deterministic `-filter_complex` graph ducking a user CC0 bed under narration.
75
+ * Narration mixes onto `[narr]` (same rules as {@link buildFilterGraph}), splits
76
+ * via `asplit` into `[nkey]` (sidechain key) and `[nmix]` (audible mix input);
77
+ * the bed loops to the narration length, ducks under `[nkey]` via
78
+ * `sidechaincompress`, then joins `[nmix]` with `amix … normalize=0` onto
79
+ * `[aout]`. Each label is defined once and consumed once as input, so ffmpeg
80
+ * never sees a doubly-consumed `[narr]`.
81
+ *
82
+ * The bed is re-validated through the schema single source first: a raw
83
+ * `{ wav }` that never passed `parseMusicBed`/`validateMusicBed` (traversal,
84
+ * non-wav, bad level) throws here instead of reaching ffmpeg.
85
+ */
86
+ export declare const buildMusicFilterGraph: (clips: readonly MixClip[], music: MusicBed) => string;
87
+ /** Role of a frozen wav; the role is part of the injective frozen name. */
88
+ export type FrozenRole = "narration" | "music";
89
+ /**
90
+ * AU-01 frozen approved mix layout (single authority for preview + render).
91
+ * After the approval entry-gate, the CLI copies every narration wav plus the
92
+ * bed wav into one frozen directory and mixes from those copies, so a
93
+ * mutation of the mutable sources mid-run (even restored before the post-check,
94
+ * ABA) cannot change the bytes ffmpeg consumes.
95
+ *
96
+ * The frozen name is `<role>-<1-based ordinal>-<source basename>`
97
+ * (`narration-2-intro.wav`, `music-1-intro.wav`). Role + ordinal make the map
98
+ * injective, so two approved sources that share a basename - a narration wav
99
+ * and the bed, two narration directories, Windows case aliases - never share
100
+ * one destination (ADR-0008 D1). The name holds no machine path, timestamp or
101
+ * absolute path, and the ordinal is the approved `mix.clips` order, which is
102
+ * itself hashed, so a current approval always maps to the same names.
103
+ *
104
+ * `frozenWavFor` maps one source wav to its frozen copy;
105
+ * `frozenClips`/`frozenMusic` rewrite clip/bed inputs to the frozen copies
106
+ * while preserving id/start/duration/volume.
107
+ */
108
+ export declare const frozenWavFor: (frozenDir: string, role: FrozenRole, ordinal: number, wav: string) => string;
109
+ export declare const frozenClips: (clips: readonly MixClip[], frozenDir: string) => MixClip[];
110
+ export declare const frozenMusic: (music: MusicBed, frozenDir: string) => MusicBed;
111
+ /**
112
+ * T-AU-01a current-only identity bed: maps a pinned `MusicIdentity`
113
+ * (`relativePath`, `sha256`, `volume`, `licenceEvidenceSha256`) to the
114
+ * absolute wav the mix graph consumes. The identity itself stays
115
+ * project-relative with forward slashes (no machine paths, no timestamps);
116
+ * only the resolved `wav` is absolute and only the frozen copy is consumed
117
+ * downstream. Rejects absolute/escaping/non-wav refs, malformed hashes, and
118
+ * out-of-range volumes (fail closed, never a silent fallback). Defense in
119
+ * depth: `wav` must live inside `frozenDir` (prefix + relative assert), so an
120
+ * absolute path outside the frozen layout never reaches ffmpeg.
121
+ */
122
+ export declare const musicBedFromIdentity: (identity: MusicIdentity, frozenDir: string, wav: string) => MusicBed;
123
+ /**
124
+ * Verifies frozen bytes against the pinned identity. Non-empty means refuse
125
+ * to mix (fail closed): a mutated bed sha — including an ABA restore of the
126
+ * mutable source after the freeze — still fails unless the frozen copy
127
+ * consumed by ffmpeg matches the identity.
128
+ */
129
+ export declare const verifyFrozenMusic: (identity: MusicIdentity, frozenBedSha256: string | undefined, frozenLicenceSha256: string | null | undefined) => string[];
130
+ /**
131
+ * Seamless-loop trim for a bed of `bedDuration` seconds over narration
132
+ * `clips` (phase math in `./music.ts`): repeats via `aloop` then trims to
133
+ * the narration length, reporting the loop-boundary phase error.
134
+ */
135
+ export declare const musicLoopFor: (bedDuration: number, clips: readonly MixClip[]) => SeamlessLoop;
136
+ /**
137
+ * Deterministic ffmpeg argv with a ducked bed: silent video plus one `-i`
138
+ * per narration wav plus the bed wav last, video passed through
139
+ * (`-c:v copy`), bed ducked under narration and mixed to aac, `-shortest`.
140
+ */
141
+ export declare const buildMusicMixArgs: (videoIn: string, clips: readonly MixClip[], music: MusicBed, out: string) => string[];
142
+ /**
143
+ * Delivery loudness (fixed, no CLI flag). Kokoro narration lands near -26 LUFS,
144
+ * so the mix would otherwise fail every verified platform policy; the mixed
145
+ * bed+voice file is normalized once here so preview, draft and render all
146
+ * deliver at the same level.
147
+ *
148
+ * `TARGET_TRUE_PEAK_DB` is loudnorm's *aim*, 0.5 dB under the platform ceiling
149
+ * so the aac encode keeps its headroom; the delivery ceiling itself stays the
150
+ * verified policy's -1 dBTP (`MIX_TRUE_PEAK_DBTP`), which is what gates a pass.
151
+ */
152
+ export declare const TARGET_LUFS = -14;
153
+ export declare const TARGET_TRUE_PEAK_DB = -1.5;
154
+ export declare const TARGET_LRA = 11;
155
+ /** Within this drift the mix already sits at delivery level (idempotence guard). */
156
+ export declare const AT_TARGET_TOLERANCE_LU = 0.5;
157
+ /** Normalizing passes per mix: the first, plus at most one corrective pass. */
158
+ export declare const MAX_NORMALIZE_PASSES = 2;
159
+ /** Pass 1 filter: prints the measurement json on stderr and passes audio through. */
160
+ export declare const LOUDNORM_MEASURE_FILTER = "loudnorm=print_format=json";
161
+ /** One loudnorm measurement, as pass 2 hands it back to pass 1's result. */
162
+ export interface LoudnessMeasurement {
163
+ readonly integratedLufs: number;
164
+ readonly truePeakDb: number;
165
+ readonly lra: number;
166
+ readonly threshold: number;
167
+ readonly offset: number;
168
+ }
169
+ /**
170
+ * Reads the last `{…}` block loudnorm prints. Unreadable or silent input
171
+ * (integrated `-inf`) yields undefined: an unmeasurable mix is never
172
+ * "already at target".
173
+ */
174
+ export declare const parseLoudnessMeasurement: (output: string) => LoudnessMeasurement | undefined;
175
+ /**
176
+ * True when the mix still needs a normalizing pass: the level is off by more
177
+ * than the 0.5 LU guard, or the peaks sit above the delivery ceiling (-1 dBTP,
178
+ * the verified platform policy). loudnorm *aims* at {@link TARGET_TRUE_PEAK_DB}
179
+ * to leave aac headroom, but its dynamic fallback can settle slightly above the
180
+ * aim while holding the level; that is delivered, not re-normalized, which is
181
+ * what keeps a second run a no-op.
182
+ */
183
+ export declare const needsNormalization: (measurement: LoudnessMeasurement) => boolean;
184
+ /** Pass 2 filter: one linear gain from the pass-1 measurements plus the target. */
185
+ export declare const buildLoudnessNormalizeFilter: (measurement: LoudnessMeasurement) => string;
186
+ /** Pass 1 argv: measure the mixed file; `-vn` keeps the video stream undecoded. */
187
+ export declare const buildLoudnessMeasureArgs: (input: string) => string[];
188
+ /**
189
+ * Pass 2 argv: rewrite `input` at the target into `out`, copying the video
190
+ * stream (`-c:v copy`, never decoded). loudnorm's own `linear` fallback keeps
191
+ * peaks under the ceiling when a single gain would overshoot.
192
+ */
193
+ export declare const buildLoudnessNormalizeArgs: (input: string, measurement: LoudnessMeasurement, out: string) => string[];
194
+ /** Exit code plus captured output of one ffmpeg run (structured like the CLI's CommandResult). */
195
+ export interface MixRunResult {
196
+ readonly code: number;
197
+ readonly stdout: string;
198
+ readonly stderr: string;
199
+ }
200
+ export type MixRunner = (argv: readonly string[]) => Promise<MixRunResult>;
201
+ /** One narrated mix at delivery level: same mix argv, same output path as before. */
202
+ export interface MixDeliveryPlan {
203
+ readonly videoIn: string;
204
+ readonly clips: readonly MixClip[];
205
+ readonly music?: MusicBed;
206
+ /** The path the mix wrote before normalization; normalization rewrites it in place. */
207
+ readonly out: string;
208
+ /** Moves the normalized temp file over `out` (fs in the CLI, a fake in tests). */
209
+ readonly swap: (from: string, to: string) => Promise<void>;
210
+ }
211
+ export interface MixDelivery {
212
+ readonly ok: boolean;
213
+ readonly out: string;
214
+ readonly normalized: boolean;
215
+ readonly measurement: LoudnessMeasurement | null;
216
+ readonly problems: readonly string[];
217
+ }
218
+ /**
219
+ * Mixes to `plan.out`, then normalizes that same file to delivery level.
220
+ *
221
+ * Silent films stay silent (no clips → no ffmpeg at all) and an already-levelled
222
+ * mix skips pass 2, so normalizing twice never moves the level. One linear gain
223
+ * cannot always reach the target under the peak ceiling (loudnorm then runs its
224
+ * dynamic fallback and can land ~0.8 LU short), so the level is re-measured
225
+ * after each pass and one corrective pass may follow: the delivered file sits
226
+ * inside the 0.5 LU guard whenever the material allows it, which is what keeps
227
+ * a second normalizing run a no-op. Unmeasurable audio is left exactly as the
228
+ * mix produced it and reported in `problems`; a failed mix or a failed loudnorm
229
+ * pass fails delivery.
230
+ */
231
+ export declare const mixAtDeliveryLevel: (run: MixRunner, plan: MixDeliveryPlan) => Promise<MixDelivery>;