@softure-ai/marketing-kit 0.0.0-stage → 0.1.5

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 (269) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +412 -2
  3. package/dist/cli/failure.d.ts +10 -0
  4. package/dist/cli/failure.d.ts.map +1 -0
  5. package/dist/cli/failure.js +16 -0
  6. package/dist/cli/failure.js.map +1 -0
  7. package/dist/cli/films.d.ts +13 -0
  8. package/dist/cli/films.d.ts.map +1 -0
  9. package/dist/cli/films.js +31 -0
  10. package/dist/cli/films.js.map +1 -0
  11. package/dist/cli/main.d.ts +3 -0
  12. package/dist/cli/main.d.ts.map +1 -0
  13. package/dist/cli/main.js +240 -0
  14. package/dist/cli/main.js.map +1 -0
  15. package/dist/cli/og.d.ts +4 -0
  16. package/dist/cli/og.d.ts.map +1 -0
  17. package/dist/cli/og.js +24 -0
  18. package/dist/cli/og.js.map +1 -0
  19. package/dist/cli/options.d.ts +48 -0
  20. package/dist/cli/options.d.ts.map +1 -0
  21. package/dist/cli/options.js +114 -0
  22. package/dist/cli/options.js.map +1 -0
  23. package/dist/cli/server.d.ts +12 -0
  24. package/dist/cli/server.d.ts.map +1 -0
  25. package/dist/cli/server.js +70 -0
  26. package/dist/cli/server.js.map +1 -0
  27. package/dist/cli/voice.d.ts +14 -0
  28. package/dist/cli/voice.d.ts.map +1 -0
  29. package/dist/cli/voice.js +46 -0
  30. package/dist/cli/voice.js.map +1 -0
  31. package/dist/compose/compose.d.ts +82 -0
  32. package/dist/compose/compose.d.ts.map +1 -0
  33. package/dist/compose/compose.js +364 -0
  34. package/dist/compose/compose.js.map +1 -0
  35. package/dist/compose/timeline.d.ts +183 -0
  36. package/dist/compose/timeline.d.ts.map +1 -0
  37. package/dist/compose/timeline.js +218 -0
  38. package/dist/compose/timeline.js.map +1 -0
  39. package/dist/config/actions-schema.d.ts +1166 -0
  40. package/dist/config/actions-schema.d.ts.map +1 -0
  41. package/dist/config/actions-schema.js +230 -0
  42. package/dist/config/actions-schema.js.map +1 -0
  43. package/dist/config/brand.d.ts +20 -0
  44. package/dist/config/brand.d.ts.map +1 -0
  45. package/dist/config/brand.js +72 -0
  46. package/dist/config/brand.js.map +1 -0
  47. package/dist/config/colors.d.ts +23 -0
  48. package/dist/config/colors.d.ts.map +1 -0
  49. package/dist/config/colors.js +39 -0
  50. package/dist/config/colors.js.map +1 -0
  51. package/dist/config/config.d.ts +124 -0
  52. package/dist/config/config.d.ts.map +1 -0
  53. package/dist/config/config.js +172 -0
  54. package/dist/config/config.js.map +1 -0
  55. package/dist/config/css-colors.d.ts +22 -0
  56. package/dist/config/css-colors.d.ts.map +1 -0
  57. package/dist/config/css-colors.js +119 -0
  58. package/dist/config/css-colors.js.map +1 -0
  59. package/dist/config/design-json.d.ts +3 -0
  60. package/dist/config/design-json.d.ts.map +1 -0
  61. package/dist/config/design-json.js +36 -0
  62. package/dist/config/design-json.js.map +1 -0
  63. package/dist/config/issues.d.ts +23 -0
  64. package/dist/config/issues.d.ts.map +1 -0
  65. package/dist/config/issues.js +41 -0
  66. package/dist/config/issues.js.map +1 -0
  67. package/dist/config/schema.d.ts +1347 -0
  68. package/dist/config/schema.d.ts.map +1 -0
  69. package/dist/config/schema.js +556 -0
  70. package/dist/config/schema.js.map +1 -0
  71. package/dist/config/screenshot-names.d.ts +22 -0
  72. package/dist/config/screenshot-names.d.ts.map +1 -0
  73. package/dist/config/screenshot-names.js +15 -0
  74. package/dist/config/screenshot-names.js.map +1 -0
  75. package/dist/film.d.ts +147 -0
  76. package/dist/film.d.ts.map +1 -0
  77. package/dist/film.js +18 -0
  78. package/dist/film.js.map +1 -0
  79. package/dist/index.d.ts +28 -0
  80. package/dist/index.d.ts.map +1 -0
  81. package/dist/index.js +33 -0
  82. package/dist/index.js.map +1 -0
  83. package/dist/messages/en.d.ts +23 -0
  84. package/dist/messages/en.d.ts.map +1 -0
  85. package/dist/messages/en.js +23 -0
  86. package/dist/messages/en.js.map +1 -0
  87. package/dist/messages/index.d.ts +49 -0
  88. package/dist/messages/index.d.ts.map +1 -0
  89. package/dist/messages/index.js +18 -0
  90. package/dist/messages/index.js.map +1 -0
  91. package/dist/messages/pl.d.ts +3 -0
  92. package/dist/messages/pl.d.ts.map +1 -0
  93. package/dist/messages/pl.js +21 -0
  94. package/dist/messages/pl.js.map +1 -0
  95. package/dist/og/character-map.d.ts +15 -0
  96. package/dist/og/character-map.d.ts.map +1 -0
  97. package/dist/og/character-map.js +156 -0
  98. package/dist/og/character-map.js.map +1 -0
  99. package/dist/og/element.d.ts +19 -0
  100. package/dist/og/element.d.ts.map +1 -0
  101. package/dist/og/element.js +18 -0
  102. package/dist/og/element.js.map +1 -0
  103. package/dist/og/fonts.d.ts +50 -0
  104. package/dist/og/fonts.d.ts.map +1 -0
  105. package/dist/og/fonts.js +99 -0
  106. package/dist/og/fonts.js.map +1 -0
  107. package/dist/og/glyphs.d.ts +23 -0
  108. package/dist/og/glyphs.d.ts.map +1 -0
  109. package/dist/og/glyphs.js +165 -0
  110. package/dist/og/glyphs.js.map +1 -0
  111. package/dist/og/index.d.ts +12 -0
  112. package/dist/og/index.d.ts.map +1 -0
  113. package/dist/og/index.js +12 -0
  114. package/dist/og/index.js.map +1 -0
  115. package/dist/og/palette.d.ts +25 -0
  116. package/dist/og/palette.d.ts.map +1 -0
  117. package/dist/og/palette.js +25 -0
  118. package/dist/og/palette.js.map +1 -0
  119. package/dist/og/render.d.ts +47 -0
  120. package/dist/og/render.d.ts.map +1 -0
  121. package/dist/og/render.js +141 -0
  122. package/dist/og/render.js.map +1 -0
  123. package/dist/og/result.d.ts +11 -0
  124. package/dist/og/result.d.ts.map +1 -0
  125. package/dist/og/result.js +3 -0
  126. package/dist/og/result.js.map +1 -0
  127. package/dist/og/templates/context.d.ts +25 -0
  128. package/dist/og/templates/context.d.ts.map +1 -0
  129. package/dist/og/templates/context.js +2 -0
  130. package/dist/og/templates/context.js.map +1 -0
  131. package/dist/og/templates/frame.d.ts +23 -0
  132. package/dist/og/templates/frame.d.ts.map +1 -0
  133. package/dist/og/templates/frame.js +62 -0
  134. package/dist/og/templates/frame.js.map +1 -0
  135. package/dist/og/templates/headline-chart.d.ts +5 -0
  136. package/dist/og/templates/headline-chart.d.ts.map +1 -0
  137. package/dist/og/templates/headline-chart.js +28 -0
  138. package/dist/og/templates/headline-chart.js.map +1 -0
  139. package/dist/og/templates/headline-cta.d.ts +5 -0
  140. package/dist/og/templates/headline-cta.d.ts.map +1 -0
  141. package/dist/og/templates/headline-cta.js +25 -0
  142. package/dist/og/templates/headline-cta.js.map +1 -0
  143. package/dist/og/templates/index.d.ts +17 -0
  144. package/dist/og/templates/index.d.ts.map +1 -0
  145. package/dist/og/templates/index.js +16 -0
  146. package/dist/og/templates/index.js.map +1 -0
  147. package/dist/og/templates/schemas.d.ts +37 -0
  148. package/dist/og/templates/schemas.d.ts.map +1 -0
  149. package/dist/og/templates/schemas.js +50 -0
  150. package/dist/og/templates/schemas.js.map +1 -0
  151. package/dist/platforms.d.ts +22 -0
  152. package/dist/platforms.d.ts.map +1 -0
  153. package/dist/platforms.js +32 -0
  154. package/dist/platforms.js.map +1 -0
  155. package/dist/posts/posts.d.ts +30 -0
  156. package/dist/posts/posts.d.ts.map +1 -0
  157. package/dist/posts/posts.js +31 -0
  158. package/dist/posts/posts.js.map +1 -0
  159. package/dist/record/actions.d.ts +17 -0
  160. package/dist/record/actions.d.ts.map +1 -0
  161. package/dist/record/actions.js +91 -0
  162. package/dist/record/actions.js.map +1 -0
  163. package/dist/record/record.d.ts +84 -0
  164. package/dist/record/record.d.ts.map +1 -0
  165. package/dist/record/record.js +294 -0
  166. package/dist/record/record.js.map +1 -0
  167. package/dist/render/hyperframes.d.ts +19 -0
  168. package/dist/render/hyperframes.d.ts.map +1 -0
  169. package/dist/render/hyperframes.js +25 -0
  170. package/dist/render/hyperframes.js.map +1 -0
  171. package/dist/render/preflight.d.ts +9 -0
  172. package/dist/render/preflight.d.ts.map +1 -0
  173. package/dist/render/preflight.js +27 -0
  174. package/dist/render/preflight.js.map +1 -0
  175. package/dist/render/render.d.ts +36 -0
  176. package/dist/render/render.d.ts.map +1 -0
  177. package/dist/render/render.js +114 -0
  178. package/dist/render/render.js.map +1 -0
  179. package/dist/screenshot/gates.d.ts +14 -0
  180. package/dist/screenshot/gates.d.ts.map +1 -0
  181. package/dist/screenshot/gates.js +23 -0
  182. package/dist/screenshot/gates.js.map +1 -0
  183. package/dist/screenshot/screenshot.d.ts +50 -0
  184. package/dist/screenshot/screenshot.d.ts.map +1 -0
  185. package/dist/screenshot/screenshot.js +143 -0
  186. package/dist/screenshot/screenshot.js.map +1 -0
  187. package/dist/voice/cache.d.ts +19 -0
  188. package/dist/voice/cache.d.ts.map +1 -0
  189. package/dist/voice/cache.js +41 -0
  190. package/dist/voice/cache.js.map +1 -0
  191. package/dist/voice/elevenlabs.d.ts +12 -0
  192. package/dist/voice/elevenlabs.d.ts.map +1 -0
  193. package/dist/voice/elevenlabs.js +58 -0
  194. package/dist/voice/elevenlabs.js.map +1 -0
  195. package/dist/voice/fake.d.ts +17 -0
  196. package/dist/voice/fake.d.ts.map +1 -0
  197. package/dist/voice/fake.js +27 -0
  198. package/dist/voice/fake.js.map +1 -0
  199. package/dist/voice/produce.d.ts +31 -0
  200. package/dist/voice/produce.d.ts.map +1 -0
  201. package/dist/voice/produce.js +33 -0
  202. package/dist/voice/produce.js.map +1 -0
  203. package/dist/voice/provider.d.ts +41 -0
  204. package/dist/voice/provider.d.ts.map +1 -0
  205. package/dist/voice/provider.js +3 -0
  206. package/dist/voice/provider.js.map +1 -0
  207. package/dist/voice/providers.d.ts +11 -0
  208. package/dist/voice/providers.d.ts.map +1 -0
  209. package/dist/voice/providers.js +9 -0
  210. package/dist/voice/providers.js.map +1 -0
  211. package/dist/voice/voiceover.d.ts +65 -0
  212. package/dist/voice/voiceover.d.ts.map +1 -0
  213. package/dist/voice/voiceover.js +133 -0
  214. package/dist/voice/voiceover.js.map +1 -0
  215. package/package.json +57 -4
  216. package/schema/marketing.schema.json +3479 -0
  217. package/src/cli/failure.ts +17 -0
  218. package/src/cli/films.ts +35 -0
  219. package/src/cli/main.ts +236 -0
  220. package/src/cli/og.ts +24 -0
  221. package/src/cli/options.ts +143 -0
  222. package/src/cli/server.ts +74 -0
  223. package/src/cli/voice.ts +52 -0
  224. package/src/compose/compose.ts +425 -0
  225. package/src/compose/timeline.ts +329 -0
  226. package/src/config/actions-schema.ts +250 -0
  227. package/src/config/brand.ts +83 -0
  228. package/src/config/colors.ts +53 -0
  229. package/src/config/config.ts +259 -0
  230. package/src/config/css-colors.ts +118 -0
  231. package/src/config/design-json.ts +37 -0
  232. package/src/config/issues.ts +56 -0
  233. package/src/config/schema.ts +630 -0
  234. package/src/config/screenshot-names.ts +31 -0
  235. package/src/film.ts +155 -0
  236. package/src/index.ts +165 -0
  237. package/src/messages/en.ts +22 -0
  238. package/src/messages/index.ts +24 -0
  239. package/src/messages/pl.ts +22 -0
  240. package/src/og/character-map.ts +158 -0
  241. package/src/og/element.ts +27 -0
  242. package/src/og/fonts.ts +136 -0
  243. package/src/og/glyphs.ts +175 -0
  244. package/src/og/index.ts +22 -0
  245. package/src/og/palette.ts +46 -0
  246. package/src/og/render.ts +178 -0
  247. package/src/og/result.ts +5 -0
  248. package/src/og/templates/context.ts +22 -0
  249. package/src/og/templates/frame.ts +91 -0
  250. package/src/og/templates/headline-chart.ts +42 -0
  251. package/src/og/templates/headline-cta.ts +35 -0
  252. package/src/og/templates/index.ts +31 -0
  253. package/src/og/templates/schemas.ts +65 -0
  254. package/src/platforms.ts +38 -0
  255. package/src/posts/posts.ts +62 -0
  256. package/src/record/actions.ts +113 -0
  257. package/src/record/record.ts +380 -0
  258. package/src/render/hyperframes.ts +30 -0
  259. package/src/render/preflight.ts +28 -0
  260. package/src/render/render.ts +146 -0
  261. package/src/screenshot/gates.ts +25 -0
  262. package/src/screenshot/screenshot.ts +200 -0
  263. package/src/voice/cache.ts +55 -0
  264. package/src/voice/elevenlabs.ts +66 -0
  265. package/src/voice/fake.ts +43 -0
  266. package/src/voice/produce.ts +46 -0
  267. package/src/voice/provider.ts +40 -0
  268. package/src/voice/providers.ts +19 -0
  269. package/src/voice/voiceover.ts +178 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SOFTURE
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/README.md CHANGED
@@ -1,3 +1,413 @@
1
- # Temporary Holding Version
1
+ # @softure-ai/marketing-kit
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A CLI and a library that turn a project's **real app** into a vertical film (1080×1920, one file for
4
+ Instagram Reels, TikTok and Facebook Reels) plus ready post copy for each platform.
5
+
6
+ The film is not an animation that imitates the app. Playwright walks through the real page on a phone
7
+ screen (or a desktop browser, framed as a browser window in 16:9) frame by frame while a scene types and taps; the camera follows the thumb, captions follow the
8
+ voiceover word by word, and hyperframes renders the HTML composition to MP4.
9
+
10
+ Ported from FIRE_TRACKER's `video/` pipeline (roadmap item MK-1). Everything product-specific comes
11
+ from one `marketing.json` (MK-2), the scene included as declarative actions (MK-3); the 1:1 and 16:9 formats (MK-6),
12
+ TTS providers (MK-7), screenshots (MK-4) and OG images (MK-5) build on it. Background:
13
+ [docs/03-marketing-kit.md](../../docs/03-marketing-kit.md).
14
+
15
+ ## Commands
16
+
17
+ ```bash
18
+ softure-marketing all <video> # voiceover from the cache -> recording -> render -> post copy
19
+ softure-marketing voice <video> [--commit] # voiceover; without --commit it only prints the cost estimate
20
+ softure-marketing record <video> [--today=YYYY-MM-DD] [--url=...]
21
+ softure-marketing render <video> [--quality=draft|standard|high]
22
+ softure-marketing preview <video> # the composition in the hyperframes preview
23
+ softure-marketing posts <video> # post copy only
24
+ softure-marketing og [image] # OG images (PNG) of every ogImages entry, or of one
25
+ softure-marketing shots [<id>] [--url=...] # the screenshots entries (or one), each behind its gates
26
+ ```
27
+
28
+ Every command takes `--config=<path>` (default `./marketing.json`). Exit codes: `0` done, `1` failed, `2`
29
+ the screen guard refused the recording (the screen did not show what the voiceover says).
30
+
31
+ - **`--commit`** is the only way to spend money: `voice` prints the cost estimate, then calls the
32
+ configured TTS provider (ElevenLabs, `ELEVENLABS_API_KEY` from the environment) and writes
33
+ `<key>.mp3` + `<key>.json` into `voice.cacheDir`. Commit them: the next render of the same text costs
34
+ nothing. `all` never pays; on a cache miss it prints the estimate and stops. See
35
+ [Voiceover providers and cost](#voiceover-providers-and-cost).
36
+ - **`--today`** records the app as of another day, only to reproduce an old film.
37
+ - **Server:** when the configured app does not answer, `record` starts `app.startCommand` in the config
38
+ folder on `app.port` and stops it afterwards (log in `<output.buildDir>/server.log`).
39
+ - Every command checks that the video's scene module exists (when it has one); `render`, `preview` and `all` also check
40
+ the brand files (logo, fonts, sound effects). A missing one is reported by its JSON path.
41
+
42
+ Output: `<output.dir>/<video>/<video>.mp4` and `posts.md`, OG images in `<output.dir>/og/<image>.png`;
43
+ recordings and compositions in `<output.buildDir>/<video>/`. None of it belongs in git.
44
+
45
+ ### Screenshots
46
+
47
+ `shots` captures every `screenshots[]` entry, or the one named, into `<output.dir>/screenshots/<id>.png`.
48
+ Each entry gets a fresh browser at `width`×`height` CSS px with `app.colorScheme`, `brand.locale`,
49
+ `brand.timezone`, `app.hideSelectors` hidden and its own `motion` preference (`reduce` by default).
50
+ `scale` sets the device pixels per CSS pixel (1-4, default 1): at `2` an 800×600 entry is a 1600×1200 PNG,
51
+ sharp on a retina screen or a store listing. `colorSchemes` (e.g. `["light", "dark"]`) captures the entry
52
+ once per scheme, in its own browser, into `<id>-light.png` and `<id>-dark.png`; without it, one `<id>.png`
53
+ in `app.colorScheme`. `shots <id>` takes the entry's id and writes all of its files. Since an entry may
54
+ write any of those three names, an id that is another entry's `<id>-light` or `<id>-dark` is refused.
55
+ `full: true` first scrolls the page one screen at a time to the bottom, so lazy images and sections
56
+ load, then captures the whole page. A screenshot is kept only when it passes every gate:
57
+
58
+ | Gate | Refused when |
59
+ | --- | --- |
60
+ | `status` | the page answers with HTTP 400 or above, or not at all (`load`: it did not load within 30 s) |
61
+ | `phrase` | the page does not show `expect` within 5 s of loading (hidden elements do not count) |
62
+ | `size` | the file is smaller than `minBytes` (40 kB by default: a blank or broken page); the file is deleted |
63
+
64
+ Each file of an entry passes the gates on its own, so a page that shows its phrase only in the dark scheme
65
+ keeps `<id>-dark.png` and refuses `<id>-light.png`. `minBytes` applies to every file as written, whatever the
66
+ `scale`: a larger scale only makes the file bigger, so the default floor stays safe.
67
+
68
+ A failed file is not left behind, nor is an older file of its entry (any of `<id>.png`, `<id>-light.png`,
69
+ `<id>-dark.png`), and the others still run; any failure ends with
70
+ exit code `1`. `--url` points at another address of the app; without it, `shots` uses `app.baseUrl` or
71
+ starts `app.startCommand` as `record` does. A plain page can be smaller than 40 kB: set `minBytes` for it
72
+ (the fixture's calculator, a dark page with one form, is about 16 kB and sets 5000).
73
+
74
+ ## `marketing.json`
75
+
76
+ The contract is one zod schema (`src/config/schema.ts`), published as
77
+ [`schema/marketing.schema.json`](schema/marketing.schema.json) (also `@softure-ai/marketing-kit/marketing.schema.json`).
78
+ Point `$schema` at it for editor completion: every key carries a description (what it does, and its default when
79
+ the schema cannot state one), so an editor or an agent sees the reference below while typing. A broken file is refused with every problem at once, each
80
+ on its JSON path (`videos[0].beats[2].id: "scene" appears twice`). Every path resolves against the
81
+ folder of `marketing.json`. A complete example: [examples/fixture/marketing.json](examples/fixture/marketing.json).
82
+
83
+ ```jsonc
84
+ {
85
+ "$schema": "https://unpkg.com/@softure-ai/marketing-kit/schema/marketing.schema.json",
86
+ "brand": {
87
+ "name": "Acme Plan",
88
+ "locale": "en-US",
89
+ "timezone": "Europe/London",
90
+ "logo": { "svg": "brand/mark.svg" },
91
+ "tokensFrom": { "css": "../src/app/globals.css", "theme": "dark", "roles": { "cta": "success" } },
92
+ "colors": { "captionBackground": "#ecf1f7", "captionHighlight": "#047857" },
93
+ "fonts": {
94
+ "body": { "family": "Inter", "files": [{ "path": "fonts/inter.woff2", "weight": "100 900" }] },
95
+ "heading": { "family": "Lora", "fallback": "serif", "files": [{ "path": "fonts/lora-600.ttf", "weight": 600 }] }
96
+ }
97
+ },
98
+ "app": {
99
+ "baseUrl": "http://localhost:3000",
100
+ "port": 3100,
101
+ "startCommand": ["npx", "next", "dev", "-p", "{port}"],
102
+ "colorScheme": "dark",
103
+ "hideSelectors": ["nextjs-portal"],
104
+ "screenGuardSelector": "main",
105
+ "device": { "viewport": [390, 844], "scale": 3 }
106
+ },
107
+ "voice": { "voiceId": "<ElevenLabs voice id>", "language": "en", "tempo": 1.1 },
108
+ "videos": [{
109
+ "id": "calculator-tour", "title": "Anna counts her date", "path": "/calculator",
110
+ "persona": { "name": "Anna", "age": 36, "tagline": "works out when she can stop working" },
111
+ "beats": [
112
+ { "id": "hook", "text": "Forty-nine years. That is when Anna stops working." },
113
+ { "id": "age", "text": "She types her age and taps next.", "actions": [
114
+ { "do": "fill", "input": "age", "value": "36" },
115
+ { "do": "until", "word": "taps" },
116
+ { "do": "tap", "target": { "role": "button", "name": { "regex": "^Next$" } }, "after": 0.3 },
117
+ { "do": "mark", "name": "exit-age", "target": { "testId": "exit-age" } },
118
+ { "do": "focus", "target": [{ "text": "Exit age", "exact": true }, { "testId": "exit-age" }], "scale": 1.4 },
119
+ { "do": "checkScreen" },
120
+ { "do": "still", "name": "result" } ] },
121
+ { "id": "cta", "text": "Count your own date.", "pad": 1.2, "actions": [{ "do": "wide" }] }
122
+ ],
123
+ "hook": { "still": "result", "shots": [{ "mark": "exit-age", "scale": 1.6 }] },
124
+ "screenGuard": ["49 years"],
125
+ "endCard": { "headline": "Count your date", "url": "example.com/calculator", "note": "Free, no account" }
126
+ }],
127
+ "social": {
128
+ "linkTemplate": "https://example.com/calculator?ref={code}",
129
+ "platforms": { "instagram": { "code": "ig-01" }, "facebook": { "code": "fb-01" } },
130
+ "posts": [{ "video": "calculator-tour", "caption": "Anna is 36 and can stop at 49.", "hashtags": ["money"] }]
131
+ },
132
+ "sfx": { "tap": "sfx/click.mp3", "key": "sfx/key.mp3", "whoosh": "sfx/whoosh.mp3", "sparkle": "sfx/sparkle.mp3", "pop": "sfx/pop.mp3" },
133
+ "output": { "dir": "marketing/out", "buildDir": "marketing/build", "quality": "standard" }
134
+ }
135
+ ```
136
+
137
+ | Section | Keys (default) | Meaning |
138
+ | --- | --- | --- |
139
+ | `brand` | `name`, `locale`, `timezone` | the end card's name; BCP 47 locale of the recording browser, `<html lang>` and the copy (its language needs a dictionary in `src/messages/`: `en`, `pl`); IANA zone of the recording browser |
140
+ | | `logo.svg` | the end card's logo next to the name (none: the name alone) |
141
+ | | `colors`, `tokensFrom` | the nine colour roles, see below |
142
+ | | `fonts.body`, `fonts.heading` | `family`, `fallback` (`sans-serif`), `files`: `path`, `weight` (`400` or `"100 900"`), `style` (`normal`), `unicodeRange`; none: the system's sans-serif. The heading font is the end card's and the avatar's; without one, the body font |
143
+ | `app` | `baseUrl`, `port`, `startCommand` | the running app, or the one the CLI starts (`startCommand` as arguments, no shell, `{port}` replaced) |
144
+ | | `colorScheme` (`light`), `hideSelectors` (`[]`), `screenGuardSelector` (`body`) | what the recording browser prefers; elements hidden while recording; the element whose text the screen guard reads |
145
+ | | `device` | the recording device: `kind` (`phone`, or `desktop` for a browser window in a 16:9 film), `viewport` `[width, height]` in CSS px (a desktop's at least 1024 wide, not taller than wide), `scale` (device pixels per CSS pixel), `mobile` (a phone's, default `true`; not allowed on a desktop); a video can override it |
146
+ | `voice` | `provider` (`elevenlabs`), `voiceId`, `model` (`eleven_multilingual_v2`), `language`, `tempo` (`1`, 0.8-1.3), `cacheDir` (`marketing/voiceover`) | the voiceover; text, voice, model and language make the cache key, the tempo is applied at build time |
147
+ | `videos[]` | `id`, `title`, `path`, `format` (`9:16`, or `1:1`, `16:9`), `device`, `voice` (`voiceId`, `model`, `tempo`) | a film and its overrides |
148
+ | | `persona`, `beats`, `hook`, `screenGuard`, `endCard` | the script, see [A film](#a-film) |
149
+ | | `beats[].actions`, `beats[].pad` | the scene as data, see [Scene actions](#scene-actions) |
150
+ | | `sceneModule` | instead of actions: the TS module exporting `scene` |
151
+ | `social` | `linkTemplate` | the link every post carries, `{code}` replaced by the platform's channel code |
152
+ | | `platforms` | `instagram`, `facebook`, `tiktok`, `youtube`, `linkedin`, `x`: `code`, `linkInBio` (true for Instagram, TikTok, YouTube) |
153
+ | | `posts[]` | `video`, `caption`, `hashtags`, `codes` (this video's own codes); a video without one gets no `posts.md` |
154
+ | `screenshots[]` | `id`, `path`, `width`, `height`, `full` (`false`), `expect`, `motion` (`reduce`), `minBytes` (`40000`), `scale` (`1`), `colorSchemes` | for `softure-marketing shots`, see [Screenshots](#screenshots) |
155
+ | `ogImages[]` | `id`, `template` (`headline-cta`, `headline-chart`), `size` (`[1200, 630]`), `data` | for `softure-marketing og`, see [OG images](#og-images) |
156
+ | `layout` | per layout (`9:16`, `1:1`, `16:9` for phone films; `desktop` for desktop films): `caption` (`top`, `left`, `right`, `fontSize`), `persona` (`top`, `left`, `right`), `endCard` (`top`, `left`, `right`, `headlineSize`, `phone.scale`, `phone.center`) | overrides of the layout's geometry table for every film of that layout, in frame px (`endCard.phone` is the browser window's pose in `desktop`); a missing key keeps the table's value. Values must fit the frame and each box's margins must leave at least 200 px for its text. The frame, the screen box and the camera target are fixed |
157
+ | `sfx` | `tap`, `key`, `whoosh`, `sparkle`, `pop` | sound effects; a missing one is silent |
158
+ | `output` | `dir` (`marketing/out`), `buildDir` (`marketing/build`), `quality` (`standard`) | where films go; `--quality` wins |
159
+
160
+ Channel codes follow the rule of `@softure-ai/analytics`, which counts them on the receiving app:
161
+ lowercase words joined by single dashes or underscores, at most 32 characters.
162
+
163
+ ### Brand colours
164
+
165
+ | Role | Paints |
166
+ | --- | --- |
167
+ | `background` | the frame behind the phone and the vignette (`#rrggbb`) |
168
+ | `foreground`, `muted` | the end card and the persona card's text |
169
+ | `accent` | the touch ring, the end of the avatar's gradient |
170
+ | `cta`, `onCta` | the end card's link pill and its text; the avatar |
171
+ | `captionBackground`, `captionText`, `captionHighlight` | the caption pill, its words, the word being spoken |
172
+
173
+ A colour in `brand.colors` wins. Otherwise the role reads a token from `brand.tokensFrom`: the app's
174
+ stylesheet (`css`, custom properties in `[data-theme="<theme>"]`, then `:root`, `var()` resolved; a
175
+ stylesheet with other themes but not this one is an error) or an
176
+ Impeccable `design.json` (`designJson`, schemaVersion 2, `themes.<theme>.roles`). A role reads the token
177
+ of its own kebab name (`onCta` reads `on-cta`) unless `tokensFrom.roles` names another one, e.g.
178
+ `"roles": { "cta": "accessible", "onCta": "background" }`. A role with no colour is an error, never a
179
+ default. Values must be hex literals (`#rgb`, `#rrggbb`, `#rrggbbaa`).
180
+
181
+ ### From FIRE_TRACKER's constants
182
+
183
+ | What MK-1 still had in code | Where it is now |
184
+ | --- | --- |
185
+ | `marketing.config.json` (`locale`, `brand.name`, `app`, `siteCss`, `posts.site`, `paths`) | `marketing.json`: `brand`, `app`, `brand.tokensFrom.css`, `social.linkTemplate`, `voice.cacheDir`, `output`, `sfx`, `brand.fonts` |
186
+ | film modules with data and scene | the data in `videos[]`, the scene in `beats[].actions` (or `sceneModule`, `export const scene: Scene`) |
187
+ | phone 390×844 @3, mobile | `app.device` |
188
+ | `pl-PL`, `Europe/Warsaw`, dark scheme | `brand.locale`, `brand.timezone`, `app.colorScheme` |
189
+ | hidden `nextjs-portal` and the mailing-list pill | `app.hideSelectors` |
190
+ | the screen guard reading `main` | `app.screenGuardSelector` |
191
+ | Geist and Newsreader files | `brand.fonts` |
192
+ | the arc logo, `#059669` and the light caption pill | `brand.logo`, `brand.colors.captionHighlight`, `captionBackground` |
193
+ | `--accessible` for the link pill and the avatar | `brand.colors.cta` (or `tokensFrom.roles.cta: "accessible"`) |
194
+ | FIRE's narrator voice, Polish | `voice.voiceId`, `voice.language: "pl"` (the cache keys of FIRE's paid recordings stay the same) |
195
+ | `?z=` and three fixed platforms | `social.linkTemplate`, `social.platforms` |
196
+ | five fixed sound file names | `sfx` |
197
+
198
+ ## A film
199
+
200
+ A film is a `videos[]` entry: the script and the scene, either as beat `actions` or as a scene module.
201
+ The fixture has the same film both ways in [examples/fixture/marketing.json](examples/fixture/marketing.json):
202
+ `fixture-tour-actions` with actions, `fixture-tour` with the module
203
+ [examples/fixture/films/fixture-tour.ts](examples/fixture/films/fixture-tour.ts). Both record the same log.
204
+
205
+ - **`beats`**: the voiceover sentences. The first plays over the opening (a frame of the result with a
206
+ rewind), the last ends on the end card. Changing the text means a new, paid recording.
207
+ - **`actions`** on every beat after the first: what happens on screen during that sentence (below).
208
+ - **`sceneModule`**, the escape hatch for a scene that needs logic: a TS module exporting `scene`
209
+ (typed `Scene`) that drives the Director itself: `d.beat(id, …, { pad })`, then the same methods as
210
+ the actions (`d.fill(name, value)`, `d.tap(locator)`, `d.until(word)`, …). A video uses one or the other.
211
+ - **`screenGuard`**: every number the voiceover says, as the screen writes it. If the screen does not
212
+ show one, the recording stops with code 2 and no film is made.
213
+
214
+ ### Scene actions
215
+
216
+ Each action is `{ "do": "<name>", …arguments }` and calls the Director method of the same name.
217
+ Optional arguments left out keep the Director's defaults.
218
+
219
+ | `do` | Arguments (default) | What it does |
220
+ | --- | --- | --- |
221
+ | `wide` | `scale` (`1`), `whoosh` (`false`) | camera on the whole screen |
222
+ | `tap` | `target`, `after` (`0.35` s) | scrolls the element into view if needed and taps its centre (clicks it on a desktop) |
223
+ | `type` | `text`, `perChar` (`0.13` s) | types into the focused element, one key at a time |
224
+ | `fill` | `input`, `value` | taps `input[name=<input>]`, moves the camera onto it (1.55×; on a desktop at most what still fits the frame) and types the value |
225
+ | `blur` | | takes the focus off the active element |
226
+ | `focus` | `target` (one or many), `scale` (fits the element), `height` | camera on the element, or on the rectangle around several |
227
+ | `bring` | `target`, `top` (`140` px), `seconds` (`0.5`) | scrolls so the element's top edge stands `top` px from the top |
228
+ | `mark` | `name`, `target` (one or many) | remembers the rectangle, e.g. for an opening shot |
229
+ | `still` | `name` | remembers the current frame as the opening frame |
230
+ | `cue` | `name` (`sparkle`, `persona-out`) | an event on the film's timeline |
231
+ | `hold` | `seconds` (0-30) | lets the screen run |
232
+ | `until` | `word` | waits until the voiceover says this word of the sentence |
233
+ | `checkScreen` | | the screen guard, now |
234
+
235
+ A beat's `pad` (`0.35` s) is how long the screen holds after the voiceover ends the sentence.
236
+
237
+ A **target** is a locator descriptor with exactly one of these keys, plus `nth` (0 = the first match):
238
+
239
+ | Descriptor | Playwright | Options |
240
+ | --- | --- | --- |
241
+ | `{ "role": "button", "name": "Next" }` | `getByRole` | `name` (the accessible name), `exact` |
242
+ | `{ "text": "Your wealth today" }` | `getByText` | `exact` |
243
+ | `{ "label": "Age" }` | `getByLabel` | `exact` |
244
+ | `{ "testId": "exit-age" }` | `getByTestId` | |
245
+ | `{ "css": "label", "hasText": "I want to know" }` | `locator` | `hasText` |
246
+
247
+ `name`, `text`, `label` and `hasText` take a string (a case-insensitive substring; with `exact: true` the
248
+ whole text, case-sensitive) or a regex: `{ "regex": "^Next$", "flags": "i" }` (flags from `imsu`).
249
+ Without `nth`, a target that matches several elements fails while recording: add `nth`, or narrow it.
250
+
251
+ Actions cover what FIRE's film uses, not all of Playwright: no chained or filtered locators, no
252
+ `getByPlaceholder`, `getByAltText` or `getByTitle`, no role options beyond `name` and `exact`, no regex
253
+ `testId`; numbers stay in ranges that catch unit slips (`scale` 0.5-4, `after`, `perChar` and `seconds`
254
+ up to 10 s, `hold` up to 30 s, whole pixels for `top` and `height`). A scene that needs more is a
255
+ `sceneModule`.
256
+
257
+ What can be checked without a browser is checked when the config loads, by JSON path: an `until` word
258
+ the sentence does not say, a `hook.still` or `hook.shots[].mark` no action saves, an opening shot after
259
+ the first without the `word` it starts on, a scene without `checkScreen`, actions on the opening sentence,
260
+ actions next to a `sceneModule`. The checks that span
261
+ sentences run once the rest of the config is valid, so fixing one round of errors can reveal the next.
262
+ An action that fails while recording names its path and the config file:
263
+
264
+ ```text
265
+ ✗ videos[0].beats[1].actions[2] (tap): sentence "age": getByRole('button', { name: /^Next$/ }) did not appear within 5 s. …
266
+ ```
267
+
268
+ ## Voiceover providers and cost
269
+
270
+ The voiceover goes through a `TtsProvider` (`src/voice/provider.ts`): `estimate(input)` is free and
271
+ needs no key, `synthesize(input)` returns the audio and the time of every word, or an error value.
272
+ `input` is `{ text, voiceId, model, language }`, and those four make the cache key
273
+ (`sha256({text, voice, model, lang})`, 16 hex characters). The tempo is applied at build time, so
274
+ changing it costs nothing.
275
+
276
+ | Run | What happens |
277
+ | --- | --- |
278
+ | the cache has `<key>.mp3` and `<key>.json` | `voiceover: from the cache <key>, nothing spent.` |
279
+ | no cache, no `--commit` | `voiceover estimate (elevenlabs): 412 characters, at most 412 ElevenLabs credits.` and a dry-run line; no request is sent |
280
+ | no cache, `--commit` | the same estimate line **first**, then one paid request, then the files are written |
281
+
282
+ ElevenLabs bills per input character, at most one credit each; API plans may discount it, so the
283
+ estimate is an upper bound. Providers available to `voice.provider`: `elevenlabs`. For tests, the
284
+ package exports `createFakeTtsProvider()` (deterministic audio, evenly spaced words, records its calls)
285
+ and `produceVoiceover({ cacheDir, input, provider, isCommit, log })`, so a project can test its films
286
+ without the network. A second real provider must add its id to the cache key, so that its recordings
287
+ never collide with ElevenLabs's under the same voice and model names.
288
+
289
+ ### Migrating FIRE_TRACKER's voiceover cache
290
+
291
+ FIRE's key hashed the same object with the language fixed to `pl`, and its words files have the same
292
+ format, so its paid recordings are reused as they are, with no re-keying and no new paid call:
293
+
294
+ 1. Copy FIRE's voiceover folder (`<key>.mp3` + `<key>.json` pairs) into `voice.cacheDir`.
295
+ 2. Set `voice.language` to `"pl"`, and `voice.voiceId` and `voice.model` (or a video's `voice`
296
+ override) to the values FIRE used.
297
+ 3. Run `softure-marketing voice <video>` **without** `--commit` for every video. Each must print
298
+ `from the cache`; an estimate line means the text, voice or model differs from FIRE's, and nothing
299
+ was spent.
300
+
301
+ ## OG images
302
+
303
+ `softure-marketing og` renders each `ogImages` entry with [Satori](https://github.com/vercel/satori)
304
+ and resvg to `<output.dir>/og/<id>.png`, at `size` (1200×630 by default), outside Next. The brand
305
+ supplies everything around the copy: the background and text colours, the logo and name in the top
306
+ corner, and the fonts. `data` is the template's input, checked by its own schema (each template's
307
+ fields are in the JSON Schema):
308
+
309
+ | Template | `data` |
310
+ | --- | --- |
311
+ | `headline-cta` | `headline` (≤ 90 characters), `eyebrow` (≤ 40), `cta` (≤ 32, a pill in `cta`/`onCta` colours), `tiles` (≤ 4 of `label` ≤ 24, `value` ≤ 16) |
312
+ | `headline-chart` | `headline`, `eyebrow`, `tiles` (≤ 3), `chart`: `viewBox` `[width, height]` and `paths` (1-8) of `d` (SVG path data), `tone` (`accent`, `cta`, `foreground`, `muted`), `fill` (a tint instead of a line), `strokeWidth` (view box units) |
313
+
314
+ Values the app computes, such as a chart or a projected date, are computed by the app and arrive in
315
+ `data` as numbers, text or SVG paths; the package only draws them.
316
+
317
+ **Fonts.** Satori reads static `.ttf`, `.otf` and `.woff` files only, so OG images refuse a `.woff2`
318
+ file or a variable range (`"100 900"`) in `brand.fonts`, by its JSON path; add a static file for OG
319
+ next to it. Templates ask for a weight (the headline for 700, the copy for 400 and 600) and get the
320
+ nearest one the brand loads, so a card never names a weight that is not loaded (Satori would draw
321
+ another one silently). Satori draws nothing, or an empty box, for a character no font maps, so a
322
+ card is checked before layout: a character of the copy (or of `brand.name`) that none of the fonts
323
+ Satori would try has is refused with the image id, the JSON path and the characters, e.g.
324
+ `ogImages[0].data.headline: "…" (U+0105)` for a Polish letter with a `latin` subset file.
325
+ Subset files work: list `latin` and `latin-ext` (or more) for each weight, as Fontsource ships them,
326
+ and a text tries them in the order listed, then the other family. Satori uses one file per family,
327
+ weight and style, so the second file of a weight and style is registered as the family `<family> #2`
328
+ (the third as `#3`) and templates write `font-family: <family>, <family> #2`; `unicodeRange` does
329
+ not steer this, the first listed file that has the character draws it. A subset file must exist at
330
+ every weight the copy uses: a letter only a `latin-ext` file of another weight has would come out
331
+ lighter or heavier than its line, so it is refused like a missing one. Whitespace, format characters
332
+ and variation selectors are not checked; emoji are, and need a font that has them.
333
+
334
+ **A thin Next route.** The `@softure-ai/marketing-kit/og` entry does not load Playwright, so a route
335
+ can render the same card per request, with live values in place of the configured `data`:
336
+
337
+ ```ts
338
+ // app/calculator/opengraph-image.ts
339
+ import { join } from "node:path";
340
+ import { loadMarketingConfig, renderConfiguredOgImage } from "@softure-ai/marketing-kit/og";
341
+
342
+ export const runtime = "nodejs"; // resvg is a native module
343
+ export const size = { width: 1200, height: 630 };
344
+ export const contentType = "image/png";
345
+
346
+ export default async function Image(): Promise<Response> {
347
+ const loaded = loadMarketingConfig(join(process.cwd(), "marketing.json"));
348
+ if (!loaded.ok) throw new Error(loaded.error);
349
+ const png = await renderConfiguredOgImage({
350
+ config: loaded.config,
351
+ id: "calculator",
352
+ data: { headline: "Stop working at 49", cta: "Count your date" }, // optional: per-request values
353
+ });
354
+ if (!png.ok) throw new Error(png.error);
355
+ return new Response(new Uint8Array(png.value), { headers: { "content-type": contentType } });
356
+ }
357
+ ```
358
+
359
+ `@resvg/resvg-js` is a native module: if the bundler tries to bundle it, list it in
360
+ `serverExternalPackages` in `next.config.ts`. `renderOgImage({ template, data, size, brand, fonts })`
361
+ renders without a `marketing.json` at all.
362
+
363
+ ## Requirements
364
+
365
+ - Node 22, **ffmpeg** in PATH.
366
+ - A Chromium for the recording and the screenshots: Playwright's own, or `PLAYWRIGHT_CHROMIUM_PATH=<path>`.
367
+ - A Chrome for hyperframes: downloaded on the first render (into `~/.cache/puppeteer`), or
368
+ `HYPERFRAMES_BROWSER_PATH=<path>` (a Chromium headless shell works).
369
+ - The CLI runs hyperframes with `HYPERFRAMES_NO_TELEMETRY=1` unless you set it yourself.
370
+
371
+ ## Licences
372
+
373
+ | Asset | Licence | How the package handles it |
374
+ | --- | --- | --- |
375
+ | hyperframes `0.8.85` | Apache-2.0 | npm dependency, pinned, run from `node_modules` |
376
+ | GSAP | GreenSock's standard "no charge" licence | npm dependency `gsap`; `gsap.min.js` is copied into the project's build folder at render time, never shipped in this package |
377
+ | Fonts | the project's | not bundled; `brand.fonts` names the files, copied next to the composition at render time |
378
+ | Sound effects | the project's | not bundled; `sfx` names the files |
379
+ | ElevenLabs | paid API | key from `ELEVENLABS_API_KEY`, only with `--commit` |
380
+ | satori, @resvg/resvg-js | MPL-2.0 | npm dependencies, unmodified; resvg ships a prebuilt native binary per platform |
381
+
382
+ ## Limitations
383
+
384
+ - Actions have no conditions or loops; a scene that needs them stays a `sceneModule`.
385
+ - Three formats: `9:16` (1080×1920), `1:1` (1080×1080) and `16:9` (1920×1080). A phone film is a framed phone laid
386
+ out by the geometry table in `src/compose/timeline.ts` (in 16:9 the phone stands left, the copy right);
387
+ one phone recording renders in every format; `layout` in `marketing.json` moves the copy and the end card, not the
388
+ phone. A desktop film (`device.kind: "desktop"`) is 16:9 only: the recorder opens a desktop browser (no touch,
389
+ mouse clicks) and the film frames it as a browser window whose address bar shows the end card's URL. The same
390
+ scene can record both when the app is responsive. Camera scales are relative to the screen, so an element as wide
391
+ as a desktop page needs a lower scale (about 1.2) than on a phone, or the zoom crops it.
392
+ - ElevenLabs is the only real voice provider; the estimate is an upper bound in credits, not money.
393
+ - Two OG templates.
394
+
395
+ ## Development
396
+
397
+ ```bash
398
+ npm test # unit tests (FIRE's, ported), the contract, the architecture test
399
+ npm run schema -w @softure-ai/marketing-kit # regenerate schema/marketing.schema.json after changing the schema
400
+ MARKETING_KIT_RENDER=1 \
401
+ PLAYWRIGHT_CHROMIUM_PATH=... HYPERFRAMES_BROWSER_PATH=... \
402
+ npx vitest run tools/marketing-kit/tests/render.test.ts # the fixture film end to end (~1.5 min)
403
+ ```
404
+
405
+ The render test copies [examples/fixture/](examples/fixture/) into a temporary folder, generates a tone
406
+ as its voiceover and tones as its sound effects with ffmpeg, runs `softure-marketing all` and checks the
407
+ MP4 with ffprobe. `MARKETING_KIT_KEEP=1` keeps the folder. CI runs it on every push in the `render` job of
408
+ [ci.yml](../../.github/workflows/ci.yml), with hyperframes on its own chrome-headless-shell.
409
+
410
+ The screenshot tests (`tests/screenshot.test.ts`, `tests/shots-cli.test.ts`) drive a browser against
411
+ static pages and the fixture app. They run whenever a Chromium is available (`PLAYWRIGHT_CHROMIUM_PATH`
412
+ or Playwright's own) and fail if `PLAYWRIGHT_CHROMIUM_PATH` names a missing file; CI points it at the
413
+ runner's Chrome.
@@ -0,0 +1,10 @@
1
+ /**
2
+ * An expected failure of a command: printed as one line without a stack, with its exit code. Any
3
+ * other error is a bug and is printed with its stack.
4
+ */
5
+ export declare class CliFailure extends Error {
6
+ readonly exitCode: number;
7
+ constructor(message: string, exitCode?: number);
8
+ }
9
+ export declare function fail(message: string, exitCode?: number): never;
10
+ //# sourceMappingURL=failure.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure.d.ts","sourceRoot":"","sources":["../../src/cli/failure.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,qBAAa,UAAW,SAAQ,KAAK;IAGjC,QAAQ,CAAC,QAAQ;gBADjB,OAAO,EAAE,MAAM,EACN,QAAQ,SAAI;CAKxB;AAED,wBAAgB,IAAI,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,SAAI,GAAG,KAAK,CAEzD"}
@@ -0,0 +1,16 @@
1
+ /**
2
+ * An expected failure of a command: printed as one line without a stack, with its exit code. Any
3
+ * other error is a bug and is printed with its stack.
4
+ */
5
+ export class CliFailure extends Error {
6
+ exitCode;
7
+ constructor(message, exitCode = 1) {
8
+ super(message);
9
+ this.exitCode = exitCode;
10
+ this.name = "CliFailure";
11
+ }
12
+ }
13
+ export function fail(message, exitCode = 1) {
14
+ throw new CliFailure(message, exitCode);
15
+ }
16
+ //# sourceMappingURL=failure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"failure.js","sourceRoot":"","sources":["../../src/cli/failure.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,MAAM,OAAO,UAAW,SAAQ,KAAK;IAGxB;IAFX,YACE,OAAe,EACN,WAAW,CAAC;QAErB,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,aAAQ,GAAR,QAAQ,CAAI;QAGrB,IAAI,CAAC,IAAI,GAAG,YAAY,CAAC;IAC3B,CAAC;CACF;AAED,MAAM,UAAU,IAAI,CAAC,OAAe,EAAE,QAAQ,GAAG,CAAC;IAChD,MAAM,IAAI,UAAU,CAAC,OAAO,EAAE,QAAQ,CAAC,CAAC;AAC1C,CAAC"}
@@ -0,0 +1,13 @@
1
+ import { type MarketingConfig, type VideoConfig } from "../config/config.js";
2
+ import type { Film } from "../film.js";
3
+ /** A video from `marketing.json` with its scene ready to record. */
4
+ export type LoadedFilm = VideoConfig & Film & {
5
+ scenePath: string;
6
+ };
7
+ export declare function listFilms(config: MarketingConfig): string[];
8
+ /**
9
+ * The video's entry and its scene: the beats' `actions`, or `sceneModule` (TypeScript through tsx).
10
+ * `scenePath` is the file to fix when the scene fails: the module, or the config for actions.
11
+ */
12
+ export declare function loadFilm(config: MarketingConfig, id: string): Promise<LoadedFilm>;
13
+ //# sourceMappingURL=films.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"films.d.ts","sourceRoot":"","sources":["../../src/cli/films.ts"],"names":[],"mappings":"AAIA,OAAO,EAAmD,KAAK,eAAe,EAAE,KAAK,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAC9H,OAAO,KAAK,EAAE,IAAI,EAAS,MAAM,YAAY,CAAC;AAI9C,oEAAoE;AACpE,MAAM,MAAM,UAAU,GAAG,WAAW,GAAG,IAAI,GAAG;IAAE,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC;AAEpE,wBAAgB,SAAS,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,EAAE,CAE3D;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,CAAC,MAAM,EAAE,eAAe,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAUvF"}
@@ -0,0 +1,31 @@
1
+ import { pathToFileURL } from "node:url";
2
+ import { tsImport } from "tsx/esm/api";
3
+ import { findMissingFiles, findVideo, formatConfigIssues } from "../config/config.js";
4
+ import { createActionScene } from "../record/actions.js";
5
+ import { fail } from "./failure.js";
6
+ export function listFilms(config) {
7
+ return config.videos.map((video) => video.id).sort();
8
+ }
9
+ function isScene(value) {
10
+ return typeof value === "function";
11
+ }
12
+ /**
13
+ * The video's entry and its scene: the beats' `actions`, or `sceneModule` (TypeScript through tsx).
14
+ * `scenePath` is the file to fix when the scene fails: the module, or the config for actions.
15
+ */
16
+ export async function loadFilm(config, id) {
17
+ const video = findVideo(config, id);
18
+ if (video === null)
19
+ fail(`no video "${id}" in ${config.file}. Available: ${listFilms(config).join(", ") || "none"}.`);
20
+ const missing = findMissingFiles(config, video, false);
21
+ if (missing.length > 0)
22
+ fail(formatConfigIssues(config, missing));
23
+ const source = video.sceneSource;
24
+ if (source.kind === "actions")
25
+ return { ...video, scene: createActionScene(source.beats, video.index), scenePath: config.file };
26
+ const imported = (await tsImport(pathToFileURL(source.path).href, import.meta.url));
27
+ if (!isScene(imported.scene))
28
+ fail(`videos[${video.index}].sceneModule: ${source.path} does not export a "scene" function.`);
29
+ return { ...video, scene: imported.scene, scenePath: source.path };
30
+ }
31
+ //# sourceMappingURL=films.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"films.js","sourceRoot":"","sources":["../../src/cli/films.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,OAAO,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAEvC,OAAO,EAAE,gBAAgB,EAAE,SAAS,EAAE,kBAAkB,EAA0C,MAAM,qBAAqB,CAAC;AAE9H,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAKpC,MAAM,UAAU,SAAS,CAAC,MAAuB;IAC/C,OAAO,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;AACvD,CAAC;AAED,SAAS,OAAO,CAAC,KAAc;IAC7B,OAAO,OAAO,KAAK,KAAK,UAAU,CAAC;AACrC,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,MAAuB,EAAE,EAAU;IAChE,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;IACpC,IAAI,KAAK,KAAK,IAAI;QAAE,IAAI,CAAC,aAAa,EAAE,QAAQ,MAAM,CAAC,IAAI,gBAAgB,SAAS,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,MAAM,GAAG,CAAC,CAAC;IACtH,MAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;IACvD,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;QAAE,IAAI,CAAC,kBAAkB,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;IAClE,MAAM,MAAM,GAAG,KAAK,CAAC,WAAW,CAAC;IACjC,IAAI,MAAM,CAAC,IAAI,KAAK,SAAS;QAAE,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,iBAAiB,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,CAAC,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;IAChI,MAAM,QAAQ,GAAG,CAAC,MAAM,QAAQ,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAwB,CAAC;IAC3G,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,IAAI,CAAC,UAAU,KAAK,CAAC,KAAK,kBAAkB,MAAM,CAAC,IAAI,sCAAsC,CAAC,CAAC;IAC7H,OAAO,EAAE,GAAG,KAAK,EAAE,KAAK,EAAE,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,CAAC,IAAI,EAAE,CAAC;AACrE,CAAC"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=main.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../../src/cli/main.ts"],"names":[],"mappings":""}