videowright 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 (306) hide show
  1. package/README.md +91 -0
  2. package/dist/cli/argv.d.ts +28 -0
  3. package/dist/cli/argv.d.ts.map +1 -0
  4. package/dist/cli/argv.js +115 -0
  5. package/dist/cli/argv.js.map +1 -0
  6. package/dist/cli/bin.d.ts +7 -0
  7. package/dist/cli/bin.d.ts.map +1 -0
  8. package/dist/cli/bin.js +10 -0
  9. package/dist/cli/bin.js.map +1 -0
  10. package/dist/cli/dev.d.ts +19 -0
  11. package/dist/cli/dev.d.ts.map +1 -0
  12. package/dist/cli/dev.js +104 -0
  13. package/dist/cli/dev.js.map +1 -0
  14. package/dist/cli/discover.d.ts +29 -0
  15. package/dist/cli/discover.d.ts.map +1 -0
  16. package/dist/cli/discover.js +104 -0
  17. package/dist/cli/discover.js.map +1 -0
  18. package/dist/cli/discover_project.d.ts +29 -0
  19. package/dist/cli/discover_project.d.ts.map +1 -0
  20. package/dist/cli/discover_project.js +108 -0
  21. package/dist/cli/discover_project.js.map +1 -0
  22. package/dist/cli/errors.d.ts +10 -0
  23. package/dist/cli/errors.d.ts.map +1 -0
  24. package/dist/cli/errors.js +13 -0
  25. package/dist/cli/errors.js.map +1 -0
  26. package/dist/cli/ffmpeg.d.ts +57 -0
  27. package/dist/cli/ffmpeg.d.ts.map +1 -0
  28. package/dist/cli/ffmpeg.js +122 -0
  29. package/dist/cli/ffmpeg.js.map +1 -0
  30. package/dist/cli/index.d.ts +7 -0
  31. package/dist/cli/index.d.ts.map +1 -0
  32. package/dist/cli/index.js +152 -0
  33. package/dist/cli/index.js.map +1 -0
  34. package/dist/cli/playwright_check.d.ts +44 -0
  35. package/dist/cli/playwright_check.d.ts.map +1 -0
  36. package/dist/cli/playwright_check.js +20 -0
  37. package/dist/cli/playwright_check.js.map +1 -0
  38. package/dist/cli/prompt.d.ts +13 -0
  39. package/dist/cli/prompt.d.ts.map +1 -0
  40. package/dist/cli/prompt.js +47 -0
  41. package/dist/cli/prompt.js.map +1 -0
  42. package/dist/cli/render.d.ts +60 -0
  43. package/dist/cli/render.d.ts.map +1 -0
  44. package/dist/cli/render.js +471 -0
  45. package/dist/cli/render.js.map +1 -0
  46. package/dist/cli/script_cmd.d.ts +26 -0
  47. package/dist/cli/script_cmd.d.ts.map +1 -0
  48. package/dist/cli/script_cmd.js +88 -0
  49. package/dist/cli/script_cmd.js.map +1 -0
  50. package/dist/cli/time_shim.d.ts +44 -0
  51. package/dist/cli/time_shim.d.ts.map +1 -0
  52. package/dist/cli/time_shim.js +390 -0
  53. package/dist/cli/time_shim.js.map +1 -0
  54. package/dist/cli/ts_loader.d.ts +28 -0
  55. package/dist/cli/ts_loader.d.ts.map +1 -0
  56. package/dist/cli/ts_loader.js +95 -0
  57. package/dist/cli/ts_loader.js.map +1 -0
  58. package/dist/cli/vite_helpers.d.ts +62 -0
  59. package/dist/cli/vite_helpers.d.ts.map +1 -0
  60. package/dist/cli/vite_helpers.js +273 -0
  61. package/dist/cli/vite_helpers.js.map +1 -0
  62. package/dist/index.d.ts +11 -0
  63. package/dist/index.d.ts.map +1 -0
  64. package/dist/index.js +14 -0
  65. package/dist/index.js.map +1 -0
  66. package/dist/player/hash_router.d.ts +23 -0
  67. package/dist/player/hash_router.d.ts.map +1 -0
  68. package/dist/player/hash_router.js +49 -0
  69. package/dist/player/hash_router.js.map +1 -0
  70. package/dist/player/hud.d.ts +33 -0
  71. package/dist/player/hud.d.ts.map +1 -0
  72. package/dist/player/hud.js +357 -0
  73. package/dist/player/hud.js.map +1 -0
  74. package/dist/player/index.d.ts +123 -0
  75. package/dist/player/index.d.ts.map +1 -0
  76. package/dist/player/index.js +848 -0
  77. package/dist/player/index.js.map +1 -0
  78. package/dist/player/input.d.ts +14 -0
  79. package/dist/player/input.d.ts.map +1 -0
  80. package/dist/player/input.js +90 -0
  81. package/dist/player/input.js.map +1 -0
  82. package/dist/player/slot.d.ts +22 -0
  83. package/dist/player/slot.d.ts.map +1 -0
  84. package/dist/player/slot.js +43 -0
  85. package/dist/player/slot.js.map +1 -0
  86. package/dist/player/transitions/cut.d.ts +7 -0
  87. package/dist/player/transitions/cut.d.ts.map +1 -0
  88. package/dist/player/transitions/cut.js +9 -0
  89. package/dist/player/transitions/cut.js.map +1 -0
  90. package/dist/player/transitions/fade.d.ts +7 -0
  91. package/dist/player/transitions/fade.d.ts.map +1 -0
  92. package/dist/player/transitions/fade.js +18 -0
  93. package/dist/player/transitions/fade.js.map +1 -0
  94. package/dist/player/transitions/index.d.ts +4 -0
  95. package/dist/player/transitions/index.d.ts.map +1 -0
  96. package/dist/player/transitions/index.js +4 -0
  97. package/dist/player/transitions/index.js.map +1 -0
  98. package/dist/player/transitions/slide.d.ts +6 -0
  99. package/dist/player/transitions/slide.d.ts.map +1 -0
  100. package/dist/player/transitions/slide.js +35 -0
  101. package/dist/player/transitions/slide.js.map +1 -0
  102. package/dist/script/index.d.ts +2 -0
  103. package/dist/script/index.d.ts.map +1 -0
  104. package/dist/script/index.js +2 -0
  105. package/dist/script/index.js.map +1 -0
  106. package/dist/script/script.d.ts +10 -0
  107. package/dist/script/script.d.ts.map +1 -0
  108. package/dist/script/script.js +41 -0
  109. package/dist/script/script.js.map +1 -0
  110. package/dist/segment/SegmentRunner.d.ts +52 -0
  111. package/dist/segment/SegmentRunner.d.ts.map +1 -0
  112. package/dist/segment/SegmentRunner.js +187 -0
  113. package/dist/segment/SegmentRunner.js.map +1 -0
  114. package/dist/segment/defineConfig.d.ts +6 -0
  115. package/dist/segment/defineConfig.d.ts.map +1 -0
  116. package/dist/segment/defineConfig.js +7 -0
  117. package/dist/segment/defineConfig.js.map +1 -0
  118. package/dist/segment/defineSegment.d.ts +7 -0
  119. package/dist/segment/defineSegment.d.ts.map +1 -0
  120. package/dist/segment/defineSegment.js +25 -0
  121. package/dist/segment/defineSegment.js.map +1 -0
  122. package/dist/segment/index.d.ts +5 -0
  123. package/dist/segment/index.d.ts.map +1 -0
  124. package/dist/segment/index.js +4 -0
  125. package/dist/segment/index.js.map +1 -0
  126. package/dist/timeline/index.d.ts +73 -0
  127. package/dist/timeline/index.d.ts.map +1 -0
  128. package/dist/timeline/index.js +142 -0
  129. package/dist/timeline/index.js.map +1 -0
  130. package/dist/timeline/loadAudioTrack.d.ts +18 -0
  131. package/dist/timeline/loadAudioTrack.d.ts.map +1 -0
  132. package/dist/timeline/loadAudioTrack.js +44 -0
  133. package/dist/timeline/loadAudioTrack.js.map +1 -0
  134. package/dist/timeline/loadVoiceover.d.ts +18 -0
  135. package/dist/timeline/loadVoiceover.d.ts.map +1 -0
  136. package/dist/timeline/loadVoiceover.js +38 -0
  137. package/dist/timeline/loadVoiceover.js.map +1 -0
  138. package/dist/timeline/resolveTiming.d.ts +28 -0
  139. package/dist/timeline/resolveTiming.d.ts.map +1 -0
  140. package/dist/timeline/resolveTiming.js +63 -0
  141. package/dist/timeline/resolveTiming.js.map +1 -0
  142. package/dist/timeline/validateTiming.d.ts +29 -0
  143. package/dist/timeline/validateTiming.d.ts.map +1 -0
  144. package/dist/timeline/validateTiming.js +62 -0
  145. package/dist/timeline/validateTiming.js.map +1 -0
  146. package/dist/types.d.ts +216 -0
  147. package/dist/types.d.ts.map +1 -0
  148. package/dist/types.js +6 -0
  149. package/dist/types.js.map +1 -0
  150. package/package.json +47 -0
  151. package/skill/SKILL.md +64 -0
  152. package/skill/assets/hello_world/PLAN.md +31 -0
  153. package/skill/assets/hello_world/README.md +27 -0
  154. package/skill/assets/hello_world/audio/audio_plan.md +14 -0
  155. package/skill/assets/hello_world/segments/hello_intro.ts +69 -0
  156. package/skill/assets/hello_world/segments/hello_outro.ts +71 -0
  157. package/skill/assets/hello_world/timeline.ts +15 -0
  158. package/skill/assets/hello_world/voiceover_script/script.md +10 -0
  159. package/skill/assets/install/package.json +10 -0
  160. package/skill/assets/install/tsconfig.json +23 -0
  161. package/skill/assets/styles/editorial-mono/STYLE.md +124 -0
  162. package/skill/assets/styles/editorial-mono/brand.md +85 -0
  163. package/skill/assets/styles/editorial-mono/reference/animations.jsx +752 -0
  164. package/skill/assets/styles/editorial-mono/reference/scenes.html +563 -0
  165. package/skill/assets/styles/editorial-mono/sample/bullet.ts +101 -0
  166. package/skill/assets/styles/editorial-mono/sample/content.ts +104 -0
  167. package/skill/assets/styles/editorial-mono/sample/cta.ts +113 -0
  168. package/skill/assets/styles/editorial-mono/sample/feature.ts +111 -0
  169. package/skill/assets/styles/editorial-mono/sample/grid.ts +97 -0
  170. package/skill/assets/styles/editorial-mono/sample/kinetic.ts +96 -0
  171. package/skill/assets/styles/editorial-mono/sample/section.ts +101 -0
  172. package/skill/assets/styles/editorial-mono/sample/stat.ts +128 -0
  173. package/skill/assets/styles/editorial-mono/sample/title.ts +97 -0
  174. package/skill/assets/styles/editorial-mono/sample/ui-showcase.ts +159 -0
  175. package/skill/assets/styles/editorial-mono/tokens.css +44 -0
  176. package/skill/assets/styles/iso-diagram/STYLE.md +109 -0
  177. package/skill/assets/styles/iso-diagram/brand.md +32 -0
  178. package/skill/assets/styles/iso-diagram/reference/animations.jsx +673 -0
  179. package/skill/assets/styles/iso-diagram/reference/scenes.html +427 -0
  180. package/skill/assets/styles/iso-diagram/sample/bullet.ts +144 -0
  181. package/skill/assets/styles/iso-diagram/sample/content.ts +192 -0
  182. package/skill/assets/styles/iso-diagram/sample/cta.ts +162 -0
  183. package/skill/assets/styles/iso-diagram/sample/feature.ts +205 -0
  184. package/skill/assets/styles/iso-diagram/sample/grid.ts +181 -0
  185. package/skill/assets/styles/iso-diagram/sample/kinetic.ts +102 -0
  186. package/skill/assets/styles/iso-diagram/sample/section.ts +149 -0
  187. package/skill/assets/styles/iso-diagram/sample/stat.ts +164 -0
  188. package/skill/assets/styles/iso-diagram/sample/title.ts +173 -0
  189. package/skill/assets/styles/iso-diagram/sample/ui-showcase.ts +162 -0
  190. package/skill/assets/styles/iso-diagram/tokens.css +40 -0
  191. package/skill/assets/styles/motion-engineering/STYLE.md +106 -0
  192. package/skill/assets/styles/motion-engineering/brand.md +29 -0
  193. package/skill/assets/styles/motion-engineering/reference/animations.jsx +673 -0
  194. package/skill/assets/styles/motion-engineering/reference/scenes.html +513 -0
  195. package/skill/assets/styles/motion-engineering/sample/bullet.ts +176 -0
  196. package/skill/assets/styles/motion-engineering/sample/content.ts +228 -0
  197. package/skill/assets/styles/motion-engineering/sample/cta.ts +209 -0
  198. package/skill/assets/styles/motion-engineering/sample/feature.ts +299 -0
  199. package/skill/assets/styles/motion-engineering/sample/grid.ts +190 -0
  200. package/skill/assets/styles/motion-engineering/sample/kinetic.ts +159 -0
  201. package/skill/assets/styles/motion-engineering/sample/section.ts +196 -0
  202. package/skill/assets/styles/motion-engineering/sample/stat.ts +230 -0
  203. package/skill/assets/styles/motion-engineering/sample/title.ts +219 -0
  204. package/skill/assets/styles/motion-engineering/sample/ui-showcase.ts +267 -0
  205. package/skill/assets/styles/motion-engineering/tokens.css +40 -0
  206. package/skill/assets/styles/neon-terminal/STYLE.md +105 -0
  207. package/skill/assets/styles/neon-terminal/brand.md +27 -0
  208. package/skill/assets/styles/neon-terminal/reference/animations.jsx +673 -0
  209. package/skill/assets/styles/neon-terminal/reference/scenes.html +387 -0
  210. package/skill/assets/styles/neon-terminal/sample/bullet.ts +113 -0
  211. package/skill/assets/styles/neon-terminal/sample/content.ts +117 -0
  212. package/skill/assets/styles/neon-terminal/sample/cta.ts +131 -0
  213. package/skill/assets/styles/neon-terminal/sample/feature.ts +112 -0
  214. package/skill/assets/styles/neon-terminal/sample/grid.ts +128 -0
  215. package/skill/assets/styles/neon-terminal/sample/kinetic.ts +105 -0
  216. package/skill/assets/styles/neon-terminal/sample/section.ts +96 -0
  217. package/skill/assets/styles/neon-terminal/sample/stat.ts +123 -0
  218. package/skill/assets/styles/neon-terminal/sample/title.ts +122 -0
  219. package/skill/assets/styles/neon-terminal/sample/ui-showcase.ts +127 -0
  220. package/skill/assets/styles/neon-terminal/tokens.css +39 -0
  221. package/skill/assets/styles/risograph/STYLE.md +110 -0
  222. package/skill/assets/styles/risograph/brand.md +26 -0
  223. package/skill/assets/styles/risograph/reference/animations.jsx +673 -0
  224. package/skill/assets/styles/risograph/reference/scenes.html +403 -0
  225. package/skill/assets/styles/risograph/sample/bullet.ts +124 -0
  226. package/skill/assets/styles/risograph/sample/content.ts +135 -0
  227. package/skill/assets/styles/risograph/sample/cta.ts +149 -0
  228. package/skill/assets/styles/risograph/sample/feature.ts +152 -0
  229. package/skill/assets/styles/risograph/sample/grid.ts +123 -0
  230. package/skill/assets/styles/risograph/sample/kinetic.ts +125 -0
  231. package/skill/assets/styles/risograph/sample/section.ts +130 -0
  232. package/skill/assets/styles/risograph/sample/stat.ts +145 -0
  233. package/skill/assets/styles/risograph/sample/title.ts +132 -0
  234. package/skill/assets/styles/risograph/sample/ui-showcase.ts +147 -0
  235. package/skill/assets/styles/risograph/tokens.css +39 -0
  236. package/skill/assets/styles/swiss-console/STYLE.md +107 -0
  237. package/skill/assets/styles/swiss-console/brand.md +37 -0
  238. package/skill/assets/styles/swiss-console/reference/animations.jsx +673 -0
  239. package/skill/assets/styles/swiss-console/reference/scenes.html +420 -0
  240. package/skill/assets/styles/swiss-console/sample/bullet.ts +122 -0
  241. package/skill/assets/styles/swiss-console/sample/content.ts +137 -0
  242. package/skill/assets/styles/swiss-console/sample/cta.ts +109 -0
  243. package/skill/assets/styles/swiss-console/sample/feature.ts +163 -0
  244. package/skill/assets/styles/swiss-console/sample/grid.ts +145 -0
  245. package/skill/assets/styles/swiss-console/sample/kinetic.ts +117 -0
  246. package/skill/assets/styles/swiss-console/sample/section.ts +127 -0
  247. package/skill/assets/styles/swiss-console/sample/stat.ts +148 -0
  248. package/skill/assets/styles/swiss-console/sample/title.ts +148 -0
  249. package/skill/assets/styles/swiss-console/sample/ui-showcase.ts +198 -0
  250. package/skill/assets/styles/swiss-console/tokens.css +39 -0
  251. package/skill/install/INSTALL.md +400 -0
  252. package/skill/references/audio/audio_plan.md +199 -0
  253. package/skill/references/audio/build.md +208 -0
  254. package/skill/references/audio/cue_template.md +219 -0
  255. package/skill/references/audio/ffmpeg_cookbook.md +267 -0
  256. package/skill/references/audio/music/music.md +171 -0
  257. package/skill/references/audio/music/providers/elevenlabs.md +170 -0
  258. package/skill/references/audio/music/providers/manual.md +140 -0
  259. package/skill/references/audio/music/providers/openverse.md +265 -0
  260. package/skill/references/audio/sfx/providers/elevenlabs.md +152 -0
  261. package/skill/references/audio/sfx/providers/manual.md +117 -0
  262. package/skill/references/audio/sfx/providers/openverse.md +243 -0
  263. package/skill/references/audio/sfx/sfx.md +149 -0
  264. package/skill/references/audio/styles.md +102 -0
  265. package/skill/references/audio/sync.md +237 -0
  266. package/skill/references/audio/voiceover/animation_sync.md +142 -0
  267. package/skill/references/audio/voiceover/provider_script.md +153 -0
  268. package/skill/references/audio/voiceover/providers/elevenlabs.md +288 -0
  269. package/skill/references/audio/voiceover/providers/manual.md +100 -0
  270. package/skill/references/audio/voiceover/script_writing.md +100 -0
  271. package/skill/references/audio/voiceover/style_intake.md +56 -0
  272. package/skill/references/audio/voiceover/sync_algorithm.md +167 -0
  273. package/skill/references/audio/voiceover.md +296 -0
  274. package/skill/references/audio.md +135 -0
  275. package/skill/references/authoring_segment.md +446 -0
  276. package/skill/references/create_or_edit_video.md +232 -0
  277. package/skill/references/dev_server.md +157 -0
  278. package/skill/references/export.md +145 -0
  279. package/skill/references/new_video.md +117 -0
  280. package/skill/references/project_structure.md +144 -0
  281. package/skill/references/setup.md +109 -0
  282. package/skill/references/setup_new_style.md +158 -0
  283. package/skill/references/styles.md +154 -0
  284. package/skill/references/testing.md +115 -0
  285. package/skill/references/types.md +240 -0
  286. package/src/cli/entry/components/copy_button.ts +42 -0
  287. package/src/cli/entry/components/download_modal.ts +204 -0
  288. package/src/cli/entry/components/empty_state.ts +55 -0
  289. package/src/cli/entry/components/hide_hud_tab.ts +37 -0
  290. package/src/cli/entry/components/icons.ts +31 -0
  291. package/src/cli/entry/components/top_bar.ts +69 -0
  292. package/src/cli/entry/components/video_card.ts +57 -0
  293. package/src/cli/entry/dev_frame.ts +189 -0
  294. package/src/cli/entry/entry_index.ts +16 -0
  295. package/src/cli/entry/entry_video.ts +24 -0
  296. package/src/cli/entry/index.html +12 -0
  297. package/src/cli/entry/parse_slug.ts +14 -0
  298. package/src/cli/entry/render.html +17 -0
  299. package/src/cli/entry/render_entry.ts +121 -0
  300. package/src/cli/entry/styles/base.css +45 -0
  301. package/src/cli/entry/styles/components.css +605 -0
  302. package/src/cli/entry/styles/tokens.css +44 -0
  303. package/src/cli/entry/video.html +22 -0
  304. package/src/cli/entry/views/homepage.ts +66 -0
  305. package/src/cli/entry/views/video_view.ts +286 -0
  306. package/src/cli/entry/virtual.d.ts +8 -0
@@ -0,0 +1,208 @@
1
+ # Audio Track Build
2
+
3
+ ## When this is loaded
4
+
5
+ You have an audio plan (`audio/audio_plan.md`) and are ready to render it into an audio track. This reference walks through the mux-to-approve workflow.
6
+
7
+ ## Overview
8
+
9
+ Building an audio track means running the ffmpeg command from the audio plan to produce a rendered audio file, then asking the user to approve or discard the result. All operations are bash commands and file edits -- no library code is involved.
10
+
11
+ ## Prerequisites
12
+
13
+ - The video has an `audio/audio_plan.md` with a complete Plan section (cues + final mix command).
14
+ - All source files referenced by the plan exist in `audio/originals/`.
15
+ - ffmpeg is installed and on PATH.
16
+
17
+ ## Build workflow
18
+
19
+ Run these steps from the video folder (e.g., `videos/demo_video/`).
20
+
21
+ ### Step 1: Determine the next track ID
22
+
23
+ List existing tracks and pick the next version:
24
+
25
+ ```bash
26
+ ls audio/tracks/ 2>/dev/null
27
+ ```
28
+
29
+ If no tracks exist, use `v1`. Otherwise, find the highest `vN` and use `v(N+1)`.
30
+
31
+ ### Step 2: Create the track folder
32
+
33
+ ```bash
34
+ mkdir -p audio/tracks/vN/
35
+ ```
36
+
37
+ ### Step 3: Snapshot the plan
38
+
39
+ Copy the Plan section of `audio/audio_plan.md` (everything from the top of the file down to but not including `## Log`) into `audio/tracks/vN/plan_snapshot.md`.
40
+
41
+ The snapshot is a verbatim copy. Do not modify it. This is what the sync stage reads.
42
+
43
+ **Splitting rule:** find the line that starts with `## Log` and take everything above it. If there is no `## Log` heading, snapshot the entire file.
44
+
45
+ ### Step 4: Run ffmpeg
46
+
47
+ Extract the final mix command from the Plan section. Replace `{TRACK_OUT}` with the actual output path:
48
+
49
+ ```bash
50
+ # Example: replace {TRACK_OUT} with audio/tracks/v3/track.mp3
51
+ ffmpeg -y \
52
+ -i audio/originals/voiceovers/v1/audio.mp3 \
53
+ -c:a libmp3lame -q:a 2 \
54
+ audio/tracks/v3/track.mp3
55
+ ```
56
+
57
+ **Run the command from the video folder** so that relative paths in the plan resolve correctly.
58
+
59
+ If ffmpeg fails:
60
+ - Print the full stderr output.
61
+ - Check for common issues: missing input files, invalid filter syntax, malformed paths.
62
+ - Do **not** write `track.ts` or append to the log.
63
+ - Present the error to the user and suggest next steps (fix the plan, check source files).
64
+
65
+ ### Step 5: Measure duration
66
+
67
+ Use ffprobe to get the rendered file's duration:
68
+
69
+ ```bash
70
+ ffprobe -v error -show_entries format=duration \
71
+ -of default=noprint_wrappers=1:nokey=1 \
72
+ audio/tracks/vN/track.mp3
73
+ ```
74
+
75
+ This returns a number like `28.742000`. Use it for `length_s` in track.ts.
76
+
77
+ ### Step 6: Write track.ts
78
+
79
+ Create `audio/tracks/vN/track.ts`:
80
+
81
+ ```ts
82
+ import type { AudioTrack } from "videowright";
83
+
84
+ const track: AudioTrack = {
85
+ audio_file: "./audio/tracks/v1/track.mp3",
86
+ length_s: 28.74,
87
+ timing: { perSegment: {} },
88
+ audio_plan_path: "../../audio_plan.md",
89
+ plan_snapshot_path: "./plan_snapshot.md",
90
+ created_at: "2026-05-15T14:22:00Z",
91
+ };
92
+
93
+ export default track;
94
+ ```
95
+
96
+ Notes:
97
+ - `audio_file` is relative to the **video folder** (the directory containing `timeline.ts`). For a track at `audio/tracks/vN/track.mp3`, use `"./audio/tracks/vN/track.mp3"`.
98
+ - `length_s` comes from ffprobe (step 5). Round to 2 decimal places.
99
+ - `timing` starts as `{ perSegment: {} }` for a new track. If a previous track exists and is active, copy its `timing` as a starting point. The sync stage will update it.
100
+ - `created_at` is the current ISO timestamp.
101
+ - `audio_plan_path` and `plan_snapshot_path` are relative to the track.ts file (these are metadata paths, not consumed by the render pipeline).
102
+
103
+ ### Step 7: Append a render log entry
104
+
105
+ Append to the Log section of `audio/audio_plan.md`:
106
+
107
+ ```markdown
108
+ ## YYYY-MM-DD HH:MM -- <summary>
109
+ **Change:** <what was modified or "initial build">
110
+ **Why:** <user feedback or "first render">
111
+ **Render:** audio/tracks/vN/track.mp3
112
+ ```
113
+
114
+ ### Step 8: Present the result
115
+
116
+ Print a clickable absolute `file://` link to the rendered audio file so the user can listen:
117
+
118
+ ```
119
+ Listen to the rendered track:
120
+ file:///absolute/path/to/audio/tracks/vN/track.mp3
121
+ ```
122
+
123
+ Use the absolute path so the link works in any terminal.
124
+
125
+ ### Step 9: Prompt for approval
126
+
127
+ Ask the user exactly two options:
128
+
129
+ > 1. **Approve** -- set this as the active audio track.
130
+ > 2. **Discard and request changes** -- delete this track and revise the plan.
131
+
132
+ Do not proceed without an explicit choice.
133
+
134
+ ### On Approve
135
+
136
+ 1. **Update timeline.ts** to import the new track as `default_audio_track`:
137
+
138
+ Find and replace the existing audio track import (or add one if this is the first track):
139
+
140
+ ```ts
141
+ import defaultAudioTrack from './audio/tracks/vN/track.js';
142
+ ```
143
+
144
+ And in the timeline object:
145
+
146
+ ```ts
147
+ default_audio_track: defaultAudioTrack,
148
+ ```
149
+
150
+ Note: the import path uses `.js` extension (TypeScript ESM convention), not `.ts`.
151
+
152
+ 2. **Append an activation log entry** to `audio/audio_plan.md`:
153
+
154
+ ```markdown
155
+ ## YYYY-MM-DD HH:MM -- Adopted vN as active track
156
+ ```
157
+
158
+ 3. **Proceed to sync** if the video has per-segment timing needs. Load [sync.md](sync.md) to compute the `Timing` from the track's plan snapshot and VO timing data.
159
+
160
+ ### On Discard
161
+
162
+ 1. **Delete the track folder**:
163
+
164
+ ```bash
165
+ rm -rf audio/tracks/vN/
166
+ ```
167
+
168
+ 2. **Append a discard log entry** to `audio/audio_plan.md` (the log is append-only -- never delete entries):
169
+
170
+ ```markdown
171
+ ## YYYY-MM-DD HH:MM -- Discarded vN
172
+ **Why:** <user's feedback>
173
+ ```
174
+
175
+ 3. **Ask for feedback**: "What should change?"
176
+
177
+ 4. **Revise the plan** based on feedback: edit cues, adjust volumes, change fades, etc.
178
+
179
+ 5. **Loop back to Step 1** and build again.
180
+
181
+ ## Rebuilding after plan changes
182
+
183
+ When the user requests mix changes after an approved track:
184
+
185
+ 1. Edit the Plan section of `audio/audio_plan.md` (update cues, volumes, etc.).
186
+ 2. Run the full build workflow again (Steps 1-9). The new track gets the next version number.
187
+ 3. The old track remains in `audio/tracks/` for rollback. To revert, just update the timeline.ts import to point back at the old track.
188
+
189
+ ## Validation before build
190
+
191
+ Before running ffmpeg, validate:
192
+
193
+ - All `Source:` paths in the plan point to existing folders.
194
+ - Each source folder contains an `audio.mp3` or `audio.wav` file.
195
+ - The final mix command references the correct number of inputs.
196
+ - `{TRACK_OUT}` is present in the final mix command.
197
+
198
+ If validation fails, report the specific issue and do not run ffmpeg.
199
+
200
+ ## Error handling
201
+
202
+ | Situation | Behavior |
203
+ |---|---|
204
+ | ffmpeg not found | Error with install instructions (see [../export.md](../export.md) for ffmpeg install guidance). |
205
+ | Source file missing | Report which path is missing. Suggest checking `audio/originals/`. |
206
+ | ffmpeg filter error | Print stderr. Common causes: typo in filter name, mismatched input count, missing `asetpts` after `atrim`. |
207
+ | Output file is 0 bytes | ffmpeg ran but produced no audio. Check filter graph for dead paths (stream never reaches output). |
208
+ | Plan has no final mix command | Error: "No final mix command found in audio_plan.md. Add a `### Final mix command` section." |
@@ -0,0 +1,219 @@
1
+ # Cue Template
2
+
3
+ ## When this is loaded
4
+
5
+ You are authoring or editing cues in an `audio_plan.md` file. This reference defines the per-cue labeled-field format and the ffmpeg snippet convention.
6
+
7
+ ## Cue format
8
+
9
+ Each cue is a markdown H3 heading followed by labeled fields and an ffmpeg snippet:
10
+
11
+ ```
12
+ ### Cue N -- <short name>
13
+ Source: <relative path to originals folder>
14
+ Slice: <start>-<end> in source (or "full file")
15
+ Place at: <start> in track
16
+ Volume: <prose curve>
17
+ Fades: <fade description>
18
+ Notes: <optional context>
19
+
20
+ ffmpeg snippet:
21
+ <filter graph fragment that produces a labeled stream>
22
+ ```
23
+
24
+ ### Field reference
25
+
26
+ | Field | Required | Description |
27
+ |---|---|---|
28
+ | **Source** | Yes | Relative path from the video folder to the originals subfolder. E.g., `audio/originals/voiceovers/v1/` or `audio/originals/sfx/keyboard_typing/`. Points at the folder, not the audio file -- the build step knows to use `audio.mp3` (or `audio.wav`) within. |
29
+ | **Slice** | Yes | Time range within the source file. Either `full file` or `<start>-<end>` in seconds (e.g., `1.2-5.6`). Start and end are timestamps in the source audio, not the output track. |
30
+ | **Place at** | Yes | Where this cue starts in the output track, in seconds (e.g., `3.4s` or `0s`). |
31
+ | **Volume** | Yes | Volume curve for this cue. Can be a simple constant (`100%`, `15%`) or a prose description of changes over time. See volume curves below. |
32
+ | **Fades** | Yes | Fade descriptions. Either `none` or a comma-separated list like `fade in 0-0.2s, fade out 7.4-7.6s`. Times are relative to the cue's placement in the track. |
33
+ | **Notes** | No | Free-text context about what this cue does musically or narratively. Also used by the sync stage as anchor hints for SFX/music timing. |
34
+
35
+ ### ffmpeg snippet
36
+
37
+ Each cue ends with a labeled ffmpeg filter graph fragment. The snippet:
38
+
39
+ - Takes one input stream (referenced by input index, e.g., `[0:a]`, `[1:a]`)
40
+ - Applies trim, delay, volume, and fade filters
41
+ - Produces a single labeled output stream (e.g., `[vo1]`, `[sfx1]`, `[music1]`)
42
+
43
+ The final mix command in the audio plan chains all these labeled streams together.
44
+
45
+ ## Volume curves
46
+
47
+ Volume is expressed as prose that maps directly to ffmpeg volume expressions. The Volume field is the human-readable version; the ffmpeg snippet is the machine-executable version.
48
+
49
+ ### Constant volume
50
+
51
+ ```
52
+ Volume: 100%
53
+ Volume: 15%
54
+ Volume: -6dB
55
+ ```
56
+
57
+ ### Time-varying volume (ducking)
58
+
59
+ ```
60
+ Volume: 100% from 0s, ramps to 15% over 4.5-4.7s, holds 15% until 22.0s, ramps to 100% over 22.0-22.2s
61
+ ```
62
+
63
+ This describes ducking under a voiceover that runs from 4.5s to 22.0s. The ffmpeg implementation uses a `volume` expression with `if(between(...))` or a sidechain compressor -- either is acceptable as long as the snippet matches the described curve.
64
+
65
+ ### Why prose, not raw ffmpeg
66
+
67
+ The Volume field is prose because:
68
+
69
+ 1. It is auditable -- anyone reading the plan sees the actual level changes.
70
+ 2. It is feedback-friendly -- "make cue 3 a bit quieter" maps to a Volume field edit.
71
+ 3. The ffmpeg snippet below it is the implementation -- the Volume field is the intent.
72
+
73
+ ## Voiceover cues
74
+
75
+ ### Single VO, full file
76
+
77
+ ```
78
+ ### Cue 1 -- VO (full file)
79
+ Source: audio/originals/voiceovers/v1/
80
+ Slice: full file
81
+ Place at: 0s
82
+ Volume: 100%
83
+ Fades: none
84
+ Notes: Full voiceover, see timing.json for word timings.
85
+
86
+ ffmpeg snippet:
87
+ [0:a] acopy [vo1]
88
+ ```
89
+
90
+ ### VO slice
91
+
92
+ A single VO file can supply multiple cues. Each specifies a slice of the source and a placement in the track.
93
+
94
+ ```
95
+ ### Cue 2 -- VO catchphrase
96
+ Source: audio/originals/voiceovers/v3/
97
+ Slice: 1.2-2.4
98
+ Place at: 3.4s
99
+ Volume: 100%
100
+ Fades: none
101
+ Notes: VO says "How now brown cow"
102
+
103
+ ffmpeg snippet:
104
+ [0:a] atrim=start=1.2:end=2.4, asetpts=PTS-STARTPTS, adelay=3400|3400 [vo2]
105
+ (adelay: repeat the delay value per channel with `|`, or use `adelay=3400:all=1` to apply to all channels — ffmpeg 4.4+)
106
+ ```
107
+
108
+ ```
109
+ ### Cue 5 -- VO outro
110
+ Source: audio/originals/voiceovers/v3/
111
+ Slice: full file
112
+ Place at: 9.4s
113
+ Volume: 100%
114
+ Fades: none
115
+ Notes: See word-by-word timing in audio/originals/voiceovers/v3/timing.json
116
+
117
+ ffmpeg snippet:
118
+ [0:a] adelay=9400|9400 [vo5]
119
+ ```
120
+
121
+ Both inline text in Notes and pointers to timing.json are fine. Inline text is convenient for short slices; for long VOs, pointing at timing.json keeps the plan readable.
122
+
123
+ ## SFX cues
124
+
125
+ ```
126
+ ### Cue 3 -- Keyboard typing
127
+ Source: audio/originals/sfx/keyboard_typing/
128
+ Slice: full file
129
+ Place at: 8.2s
130
+ Volume: 60%
131
+ Fades: fade in 8.2-8.4s, fade out 14.0-14.2s
132
+ Notes: Typing SFX under the terminal demo segment. Punchy entry, gentle fade.
133
+
134
+ ffmpeg snippet:
135
+ [2:a] volume=0.6, afade=t=in:st=0:d=0.2, afade=t=out:st=5.8:d=0.2, adelay=8200|8200 [sfx3]
136
+ ```
137
+
138
+ **Note on Fades vs ffmpeg times:** The Fades field uses **track-absolute** times for human readability (e.g., `fade in 8.2-8.4s` means "at 8.2s in the track"). The ffmpeg snippet uses **stream-relative** times (e.g., `afade=t=in:st=0:d=0.2` means "0s into this stream's audio, which starts at 8.2s in the track due to `adelay`"). The Fades field is the intent; the snippet is the implementation.
139
+
140
+ ## Music cues
141
+
142
+ ```
143
+ ### Cue 4 -- Background music
144
+ Source: audio/originals/music/uplift_piano/
145
+ Slice: 0-30
146
+ Place at: 0s
147
+ Volume: 100% from 0s, ramps to 15% over 4.5-4.7s, holds 15% until 22.0s, ramps to 100% over 22.0-22.2s
148
+ Fades: fade in 0-0.5s, fade out 28.0-30.0s
149
+ Notes: Music bed. Ducked under VO (4.5-22s). Beat drop at 5.3s aligns with feature reveal.
150
+
151
+ ffmpeg snippet:
152
+ [3:a] atrim=0:30, asetpts=PTS-STARTPTS,
153
+ volume='if(between(t,4.7,22),0.15, if(between(t,4.5,4.7),1-0.85*(t-4.5)/0.2, if(between(t,22,22.2),0.15+0.85*(t-22)/0.2,1)))':eval=frame,
154
+ afade=t=in:st=0:d=0.5, afade=t=out:st=28:d=2 [music4]
155
+ ```
156
+
157
+ ### Ducking
158
+
159
+ Ducking is expressed as a volume curve on the music (or ambient) cue -- not as a separate "duck" primitive. The Volume field describes the level changes in prose. The ffmpeg snippet implements it.
160
+
161
+ This keeps the plan auditable: anyone reading it sees the actual level changes. See [styles.md](styles.md) for ducking ramp guidance.
162
+
163
+ ## Looped cues
164
+
165
+ For a short SFX or music clip that needs to fill a longer duration:
166
+
167
+ ```
168
+ ### Cue 6 -- Ambient hum (looped)
169
+ Source: audio/originals/sfx/server_hum/
170
+ Slice: full file
171
+ Place at: 0s
172
+ Volume: 20%
173
+ Fades: fade in 0-1s, fade out 25-28s
174
+ Notes: Looped to 28s. Low ambient texture under the entire video.
175
+
176
+ ffmpeg snippet:
177
+ [4:a] aloop=loop=-1:size=44100*3, atrim=0:28, asetpts=PTS-STARTPTS,
178
+ volume=0.2, afade=t=in:st=0:d=1, afade=t=out:st=25:d=3 [sfx6]
179
+ ```
180
+
181
+ See [ffmpeg_cookbook.md](ffmpeg_cookbook.md) for loop recipes.
182
+
183
+ ## Final mix command structure
184
+
185
+ The final mix command references all input files and chains the per-cue labeled streams:
186
+
187
+ ```bash
188
+ ffmpeg -y \
189
+ -i audio/originals/voiceovers/v1/audio.mp3 \
190
+ -i audio/originals/sfx/keyboard_typing/audio.mp3 \
191
+ -i audio/originals/music/uplift_piano/audio.mp3 \
192
+ -filter_complex "
193
+ [0:a] acopy [vo1];
194
+ [1:a] volume=0.6, afade=t=in:st=0:d=0.2, afade=t=out:st=5.8:d=0.2, adelay=8200|8200 [sfx3];
195
+ [2:a] atrim=0:30, asetpts=PTS-STARTPTS,
196
+ volume='...' [music4];
197
+ [vo1][sfx3][music4] amix=inputs=3:duration=longest:normalize=0 [mix];
198
+ [mix] loudnorm=I=-14:TP=-1:LRA=11 [out]
199
+ " -map "[out]" -c:a libmp3lame -q:a 2 {TRACK_OUT}
200
+ ```
201
+
202
+ Key conventions:
203
+
204
+ - Input order matches the order inputs appear in the cue list.
205
+ - Each cue's snippet produces a named stream.
206
+ - All named streams feed into `amix` (or `amerge` for stereo mixing).
207
+ - `normalize=0` on amix prevents automatic normalization (the `loudnorm` filter handles it).
208
+ - The `loudnorm` filter at the end targets -14 LUFS (see [styles.md](styles.md)).
209
+ - Output codec is `libmp3lame -q:a 2` for mp3, or `pcm_s16le` for wav.
210
+
211
+ ## Input index mapping
212
+
213
+ When the same source file appears in multiple cues, it needs only one `-i` input. Cue snippets reference the correct input index:
214
+
215
+ - Input 0: first `-i` file
216
+ - Input 1: second `-i` file
217
+ - etc.
218
+
219
+ If two cues use the same VO file with different slices, they share the input index but apply different `atrim` filters.
@@ -0,0 +1,267 @@
1
+ # ffmpeg Cookbook
2
+
3
+ ## When this is loaded
4
+
5
+ You are building or editing an audio plan and need ffmpeg filter recipes. This is a reference of copy-pasteable ffmpeg patterns for common audio operations.
6
+
7
+ All examples assume audio inputs. Adjust input indices (`[0:a]`, `[1:a]`, ...) to match your `-i` order.
8
+
9
+ ## Trim and place a clip at a time offset
10
+
11
+ Extract a slice of audio and place it at a specific point in the output track.
12
+
13
+ ```
14
+ [0:a] atrim=start=1.2:end=5.6, asetpts=PTS-STARTPTS, adelay=3400|3400 [out]
15
+ ```
16
+
17
+ - `atrim=start=1.2:end=5.6` -- extract seconds 1.2 to 5.6 from the source
18
+ - `asetpts=PTS-STARTPTS` -- reset timestamps to start at 0 (required after trim)
19
+ - `adelay=3400|3400` -- delay by 3400ms (place at 3.4s in track). The `|`-separated format repeats the delay per channel. For any number of channels, use `adelay=3400:all=1` (ffmpeg 4.4+).
20
+
21
+ For a full file placed at an offset (no trim):
22
+
23
+ ```
24
+ [0:a] adelay=5000|5000 [out]
25
+ ```
26
+
27
+ For a full file placed at 0s (passthrough):
28
+
29
+ ```
30
+ [0:a] acopy [out]
31
+ ```
32
+
33
+ ## Constant volume scaling
34
+
35
+ Scale volume by a fixed factor:
36
+
37
+ ```
38
+ [0:a] volume=0.5 [out] # 50% volume
39
+ [0:a] volume=0.15 [out] # 15% volume
40
+ [0:a] volume=1.5 [out] # 150% volume (boost)
41
+ [0:a] volume=-6dB [out] # -6 dB attenuation
42
+ ```
43
+
44
+ ## Time-varying volume (ducking)
45
+
46
+ ### Simple duck: quiet during a time range
47
+
48
+ ```
49
+ [0:a] volume='if(between(t,4.5,22),0.15,1)':eval=frame [out]
50
+ ```
51
+
52
+ This sets volume to 15% between 4.5s and 22s, 100% elsewhere. The transition is instantaneous -- for smooth ramps, use the ramped version below.
53
+
54
+ ### Ramped duck: smooth transitions
55
+
56
+ ```
57
+ [0:a] volume='if(lt(t,4.5),1, if(between(t,4.5,4.7),1-0.85*(t-4.5)/0.2, if(between(t,4.7,22),0.15, if(between(t,22,22.2),0.15+0.85*(t-22)/0.2, 1))))':eval=frame [out]
58
+ ```
59
+
60
+ Breakdown:
61
+ - Before 4.5s: volume = 1.0
62
+ - 4.5s to 4.7s: linear ramp from 1.0 down to 0.15 (over 0.2s)
63
+ - 4.7s to 22.0s: volume = 0.15
64
+ - 22.0s to 22.2s: linear ramp from 0.15 back to 1.0 (over 0.2s)
65
+ - After 22.2s: volume = 1.0
66
+
67
+ ### Multiple duck zones
68
+
69
+ For ducking under multiple VO segments, chain conditions:
70
+
71
+ ```
72
+ volume='if(between(t,4.5,12),0.15, if(between(t,18,25),0.15, 1))':eval=frame
73
+ ```
74
+
75
+ Add ramps at each transition for smoothness. See [styles.md](styles.md) for recommended ramp durations (~0.2s).
76
+
77
+ ### Expression syntax notes
78
+
79
+ - `eval=frame` is required for time-varying expressions (evaluates per audio frame, not once).
80
+ - `t` is the timestamp in seconds within the filtered stream.
81
+ - `between(t,a,b)` returns 1 if `a <= t <= b`, else 0.
82
+ - `lt(t,a)` returns 1 if `t < a`.
83
+ - `gt(t,a)` returns 1 if `t > a`.
84
+ - Arithmetic operators: `+`, `-`, `*`, `/`.
85
+ - Nest `if()` calls for multi-range curves.
86
+
87
+ ## Fade in / fade out
88
+
89
+ ```
90
+ [0:a] afade=t=in:st=0:d=0.5 [out] # fade in over 0.5s starting at 0s
91
+ [0:a] afade=t=out:st=28:d=2 [out] # fade out over 2s starting at 28s
92
+ ```
93
+
94
+ Combine both:
95
+
96
+ ```
97
+ [0:a] afade=t=in:st=0:d=0.5, afade=t=out:st=28:d=2 [out]
98
+ ```
99
+
100
+ Parameters:
101
+ - `t=in` or `t=out` -- fade type
102
+ - `st` -- start time in seconds (within the stream, after any trim/delay)
103
+ - `d` -- duration of the fade in seconds
104
+
105
+ ### Fade curves
106
+
107
+ Default is linear. For smoother curves:
108
+
109
+ ```
110
+ afade=t=in:st=0:d=0.5:curve=tri # triangular
111
+ afade=t=in:st=0:d=0.5:curve=qsin # quarter sine (smooth)
112
+ afade=t=in:st=0:d=0.5:curve=esin # exponential sine
113
+ afade=t=in:st=0:d=0.5:curve=log # logarithmic
114
+ ```
115
+
116
+ For most video audio, the default linear fade is fine. Use `curve=qsin` when a smoother onset is desired.
117
+
118
+ ## Crossfade between two clips
119
+
120
+ ```
121
+ [0:a][1:a] acrossfade=d=1:c1=tri:c2=tri [out]
122
+ ```
123
+
124
+ - `d=1` -- crossfade duration of 1 second
125
+ - `c1`, `c2` -- fade curves for the outgoing and incoming clips
126
+
127
+ The crossfade overlaps the end of clip 0 with the start of clip 1. The output duration is `len(clip0) + len(clip1) - d`.
128
+
129
+ This is useful for music transitions but rarely needed for VO or SFX.
130
+
131
+ ## Loop a clip to fill a duration
132
+
133
+ Loop a short clip to fill a longer time span:
134
+
135
+ ```
136
+ [0:a] aloop=loop=-1:size=2000000, atrim=0:28, asetpts=PTS-STARTPTS [out]
137
+ ```
138
+
139
+ - `loop=-1` -- loop infinitely
140
+ - `size=N` -- loop region size in samples. ffmpeg accepts expressions, so `size=44100*3` works directly in the filter graph (evaluates to 132300 samples = 3s at 44.1 kHz). Set this to at least the source file's total sample count to loop the entire file.
141
+ - `atrim=0:28` -- trim the looped result to 28 seconds
142
+ - `asetpts=PTS-STARTPTS` -- reset timestamps
143
+
144
+ **Tip:** If you do not know the exact sample count, set `size` to a value larger than any plausible source (e.g., `size=2000000` covers ~45s at 44.1 kHz). ffmpeg loops at most the available samples, so an oversized value loops the full file. Use `ffprobe` to get the exact sample rate and duration if precision matters (see "Probe sample rate and channels" below).
145
+
146
+ For seamless loops, the source audio should be designed to loop cleanly (no click at the boundary). If there is a click, add a very short crossfade or fade at the loop point.
147
+
148
+ ## Mix N streams
149
+
150
+ ### amix (additive mixing)
151
+
152
+ ```
153
+ [vo1][sfx1][music1] amix=inputs=3:duration=longest:normalize=0 [out]
154
+ ```
155
+
156
+ - `inputs=3` -- number of input streams
157
+ - `duration=longest` -- output duration matches the longest input. Alternatives: `shortest`, `first`.
158
+ - `normalize=0` -- **important**: disables automatic per-stream normalization. Without this, amix scales each input down by 1/N, making everything quiet. Set `normalize=0` and control volume on each stream individually before mixing.
159
+
160
+ ### amerge (interleave channels)
161
+
162
+ ```
163
+ [0:a][1:a] amerge=inputs=2 [out]
164
+ ```
165
+
166
+ `amerge` interleaves channels (e.g., two mono streams into one stereo stream). For summing audio signals (layering VO + music), use `amix`, not `amerge`.
167
+
168
+ ## Final master with loudness normalization
169
+
170
+ Apply EBU R128 loudness normalization as the last step in the filter chain:
171
+
172
+ ```
173
+ [mix] loudnorm=I=-14:TP=-1:LRA=11 [out]
174
+ ```
175
+
176
+ - `I=-14` -- integrated loudness target: -14 LUFS (good for web/social media delivery)
177
+ - `TP=-1` -- true peak maximum: -1 dBTP (prevents clipping)
178
+ - `LRA=11` -- loudness range target: 11 LU
179
+
180
+ Apply `loudnorm` after all mixing and volume adjustments. It is the very last filter before output.
181
+
182
+ ### When to use loudnorm
183
+
184
+ - **Multi-source mixes** (VO + SFX + music): always. Different sources have different levels.
185
+ - **VO-only tracks**: optional. The VO is already at a consistent level from the TTS provider. Including loudnorm is fine but not required.
186
+
187
+ ### Two-pass loudnorm (higher quality)
188
+
189
+ For critical masters, run loudnorm in two-pass mode:
190
+
191
+ Pass 1 (measure):
192
+ ```bash
193
+ ffmpeg -i input.mp3 -af loudnorm=I=-14:TP=-1:LRA=11:print_format=json -f null /dev/null
194
+ ```
195
+
196
+ This prints measured loudness values. Extract `input_i`, `input_tp`, `input_lra`, `input_thresh`, `target_offset`.
197
+
198
+ Pass 2 (apply with measured values):
199
+ ```bash
200
+ ffmpeg -i input.mp3 -af loudnorm=I=-14:TP=-1:LRA=11:measured_I=-18.5:measured_TP=-2.3:measured_LRA=8.2:measured_thresh=-28.5:offset=-0.5:linear=true [out]
201
+ ```
202
+
203
+ For most video audio, single-pass loudnorm is sufficient.
204
+
205
+ ## Measure audio duration (ffprobe)
206
+
207
+ Get the duration of an audio file in seconds:
208
+
209
+ ```bash
210
+ ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 audio.mp3
211
+ ```
212
+
213
+ This outputs a single number like `28.742000`. Use this when writing `length_s` in `track.ts`.
214
+
215
+ ## Probe sample rate and channels
216
+
217
+ ```bash
218
+ ffprobe -v error -show_entries stream=sample_rate,channels -of default=noprint_wrappers=1 audio.mp3
219
+ ```
220
+
221
+ Useful for computing the `size` parameter in `aloop`.
222
+
223
+ ## Complete single-command examples
224
+
225
+ ### VO-only (passthrough)
226
+
227
+ ```bash
228
+ ffmpeg -y \
229
+ -i audio/originals/voiceovers/v1/audio.mp3 \
230
+ -c:a libmp3lame -q:a 2 \
231
+ {TRACK_OUT}
232
+ ```
233
+
234
+ ### VO + music with ducking
235
+
236
+ ```bash
237
+ ffmpeg -y \
238
+ -i audio/originals/voiceovers/v1/audio.mp3 \
239
+ -i audio/originals/music/uplift_piano/audio.mp3 \
240
+ -filter_complex "
241
+ [0:a] acopy [vo];
242
+ [1:a] atrim=0:30, asetpts=PTS-STARTPTS,
243
+ volume='if(between(t,4.7,22),0.15, if(between(t,4.5,4.7),1-0.85*(t-4.5)/0.2, if(between(t,22,22.2),0.15+0.85*(t-22)/0.2,1)))':eval=frame,
244
+ afade=t=in:st=0:d=0.5, afade=t=out:st=28:d=2 [music];
245
+ [vo][music] amix=inputs=2:duration=longest:normalize=0 [mix];
246
+ [mix] loudnorm=I=-14:TP=-1:LRA=11 [out]
247
+ " -map "[out]" -c:a libmp3lame -q:a 2 {TRACK_OUT}
248
+ ```
249
+
250
+ ### VO + SFX + music (full mix)
251
+
252
+ ```bash
253
+ ffmpeg -y \
254
+ -i audio/originals/voiceovers/v1/audio.mp3 \
255
+ -i audio/originals/sfx/keyboard_typing/audio.mp3 \
256
+ -i audio/originals/music/uplift_piano/audio.mp3 \
257
+ -filter_complex "
258
+ [0:a] acopy [vo];
259
+ [1:a] volume=0.6, afade=t=in:st=0:d=0.2, afade=t=out:st=5.8:d=0.2,
260
+ adelay=8200|8200 [sfx];
261
+ [2:a] atrim=0:30, asetpts=PTS-STARTPTS,
262
+ volume='if(between(t,4.7,22),0.15,1)':eval=frame,
263
+ afade=t=in:st=0:d=0.5, afade=t=out:st=28:d=2 [music];
264
+ [vo][sfx][music] amix=inputs=3:duration=longest:normalize=0 [mix];
265
+ [mix] loudnorm=I=-14:TP=-1:LRA=11 [out]
266
+ " -map "[out]" -c:a libmp3lame -q:a 2 {TRACK_OUT}
267
+ ```