@anvpx/motion-build 0.0.0-stage → 0.1.1

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