@vfrey/vfrey 0.0.1 → 1.0.0-beta.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 (603) hide show
  1. package/LICENSE +33 -0
  2. package/README.md +78 -3
  3. package/accounts.example.json +61 -0
  4. package/accounts.first-run.json +41 -0
  5. package/bin/vfrey.mjs +36 -0
  6. package/integration-tools/README.md +43 -0
  7. package/integration-tools/image/pillow.md +32 -0
  8. package/integration-tools/image/playwright.md +33 -0
  9. package/integration-tools/image/resvg.md +36 -0
  10. package/integration-tools/image/satori.md +36 -0
  11. package/integration-tools/image/sharp.md +38 -0
  12. package/integration-tools/video/blender.md +34 -0
  13. package/integration-tools/video/ffmpeg.md +41 -0
  14. package/integration-tools/video/manim.md +37 -0
  15. package/integration-tools/video/motion-canvas.md +37 -0
  16. package/integration-tools/video/remotion.md +50 -0
  17. package/integration-tools/video/revideo.md +32 -0
  18. package/package.json +74 -6
  19. package/skills/vfrey-orchestrator/SKILL.md +15 -0
  20. package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
  21. package/skills/vfrey-product/SKILL.md +63 -0
  22. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
  23. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
  24. package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
  25. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
  26. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
  27. package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
  28. package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
  29. package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
  30. package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
  31. package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
  32. package/skills/vfrey-product/push/SKILL.md +15 -0
  33. package/skills/vfrey-product/scenes/NOTICE +5 -0
  34. package/skills/vfrey-product/scenes/blocks.md +57 -0
  35. package/skills/vfrey-product/scenes/craft.md +118 -0
  36. package/skills/vfrey-product/scenes/motion.md +88 -0
  37. package/src/analytics/dashboard.ts +109 -0
  38. package/src/analytics/index.ts +260 -0
  39. package/src/api/client.ts +282 -0
  40. package/src/api/commands.ts +330 -0
  41. package/src/api/config.ts +34 -0
  42. package/src/api/creators.ts +463 -0
  43. package/src/api/credentials.ts +152 -0
  44. package/src/api/mail.ts +333 -0
  45. package/src/api/topics.ts +207 -0
  46. package/src/api/types.ts +205 -0
  47. package/src/cli.ts +467 -0
  48. package/src/cloud/executor.ts +281 -0
  49. package/src/cloud/job.ts +106 -0
  50. package/src/cloud/render-job.ts +187 -0
  51. package/src/config.ts +309 -0
  52. package/src/dashboard-link.ts +180 -0
  53. package/src/dashboard-start.ts +103 -0
  54. package/src/desk/channel.ts +64 -0
  55. package/src/desk/cli.ts +216 -0
  56. package/src/desk/creators-route.ts +129 -0
  57. package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
  58. package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
  59. package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
  60. package/src/desk/http.ts +460 -0
  61. package/src/desk/mail-route.ts +103 -0
  62. package/src/desk/overlay.ts +112 -0
  63. package/src/desk/preview.ts +422 -0
  64. package/src/desk/screens.ts +149 -0
  65. package/src/desk/secrets.ts +143 -0
  66. package/src/desk/series.ts +127 -0
  67. package/src/desk/state.ts +77 -0
  68. package/src/desk/studio-route.ts +119 -0
  69. package/src/desk/tick-runner.ts +66 -0
  70. package/src/desk/tick.ts +56 -0
  71. package/src/desk/topics-route.ts +41 -0
  72. package/src/desk/ui/app.css +505 -0
  73. package/src/desk/ui/app.js +185 -0
  74. package/src/desk/ui/apple-icon.png +0 -0
  75. package/src/desk/ui/components.js +79 -0
  76. package/src/desk/ui/core.js +156 -0
  77. package/src/desk/ui/favicon.ico +0 -0
  78. package/src/desk/ui/icon.svg +1 -0
  79. package/src/desk/ui/icons.js +49 -0
  80. package/src/desk/ui/index.html +41 -0
  81. package/src/desk/ui/metrics-math.d.ts +22 -0
  82. package/src/desk/ui/metrics-math.js +135 -0
  83. package/src/desk/ui/theme.js +5 -0
  84. package/src/desk/ui/views-creators.js +209 -0
  85. package/src/desk/ui/views-keys.js +114 -0
  86. package/src/desk/ui/views-mail.js +188 -0
  87. package/src/desk/ui/views-misc.js +123 -0
  88. package/src/desk/ui/views-review.js +119 -0
  89. package/src/desk/ui/views-runs.js +350 -0
  90. package/src/desk/ui/views-screens.js +163 -0
  91. package/src/desk/ui/views-series.js +318 -0
  92. package/src/desk/ui/views-topics.js +87 -0
  93. package/src/desk/verbs.ts +361 -0
  94. package/src/doctor.ts +212 -0
  95. package/src/drain.ts +14 -0
  96. package/src/engage/index.ts +217 -0
  97. package/src/generate/index.ts +110 -0
  98. package/src/generate/script.ts +152 -0
  99. package/src/generate/title.ts +78 -0
  100. package/src/help.ts +233 -0
  101. package/src/home.ts +45 -0
  102. package/src/llm/client.ts +177 -0
  103. package/src/log.ts +7 -0
  104. package/src/oauth/tokens.ts +189 -0
  105. package/src/oauth/vault.ts +72 -0
  106. package/src/pipeline/review.ts +642 -0
  107. package/src/pipeline/run.ts +443 -0
  108. package/src/pipeline/series-guard.ts +26 -0
  109. package/src/pipeline/source-fetch.ts +69 -0
  110. package/src/pipeline/stages.ts +728 -0
  111. package/src/pipeline/topics-source.ts +90 -0
  112. package/src/product/commands.ts +296 -0
  113. package/src/product/config-file.ts +38 -0
  114. package/src/product/engine-choice.ts +89 -0
  115. package/src/product/engines.ts +103 -0
  116. package/src/product/insights.ts +50 -0
  117. package/src/product/keys.ts +67 -0
  118. package/src/product/paths.ts +18 -0
  119. package/src/product/probes.ts +88 -0
  120. package/src/product/push-flow.ts +90 -0
  121. package/src/product/render-video.ts +21 -0
  122. package/src/product/short.ts +62 -0
  123. package/src/product/update.ts +32 -0
  124. package/src/project/canonical.ts +14 -0
  125. package/src/project/commands.ts +177 -0
  126. package/src/project/dashboard-status.ts +28 -0
  127. package/src/project/discover.ts +19 -0
  128. package/src/project/index-file.ts +73 -0
  129. package/src/project/marker.ts +87 -0
  130. package/src/project/migrate.ts +201 -0
  131. package/src/project/names.ts +27 -0
  132. package/src/project/notice.ts +16 -0
  133. package/src/project/open.ts +88 -0
  134. package/src/project/scaffold.ts +112 -0
  135. package/src/project/templates.ts +42 -0
  136. package/src/project/videos.ts +64 -0
  137. package/src/publish/facebook.ts +91 -0
  138. package/src/publish/index.ts +12 -0
  139. package/src/publish/instagram.ts +97 -0
  140. package/src/publish/rate.ts +62 -0
  141. package/src/publish/session.ts +242 -0
  142. package/src/publish/tiktok.ts +202 -0
  143. package/src/publish/youtube.ts +177 -0
  144. package/src/queue/desk-ops.ts +1170 -0
  145. package/src/queue/seal.ts +3 -0
  146. package/src/queue/store.ts +1006 -0
  147. package/src/remotion/AiLabel.tsx +26 -0
  148. package/src/remotion/Root.tsx +73 -0
  149. package/src/remotion/defaults.ts +26 -0
  150. package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
  151. package/src/remotion/films/engine/EngineMark.tsx +17 -0
  152. package/src/remotion/films/rea/ReaShort.tsx +552 -0
  153. package/src/remotion/films/rea/drawings.tsx +907 -0
  154. package/src/remotion/films/rea/timing.ts +144 -0
  155. package/src/remotion/index.ts +4 -0
  156. package/src/remotion/templates/EditorialSplit.tsx +39 -0
  157. package/src/remotion/templates/KineticType.tsx +39 -0
  158. package/src/remotion/templates/SignalBoard.tsx +50 -0
  159. package/src/remotion/timing.ts +22 -0
  160. package/src/render/chrome.ts +78 -0
  161. package/src/render/engine-mark.ts +29 -0
  162. package/src/render/framey-template.ts +130 -0
  163. package/src/render/html-engine.ts +149 -0
  164. package/src/render/node-crypto-stub.ts +11 -0
  165. package/src/render/probe.ts +29 -0
  166. package/src/render/props.ts +17 -0
  167. package/src/render/vertical-short.ts +35 -0
  168. package/src/render/webpack.ts +33 -0
  169. package/src/render/worker.ts +65 -0
  170. package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
  171. package/src/scene/assets/fonts/titan-one.woff2 +0 -0
  172. package/src/scene/assets/gsap/LICENSE.txt +8 -0
  173. package/src/scene/assets/gsap/gsap.min.js +11 -0
  174. package/src/scene/audio.ts +54 -0
  175. package/src/scene/blocks/LICENSE +190 -0
  176. package/src/scene/blocks/NOTICE +5 -0
  177. package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
  178. package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
  179. package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
  180. package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
  181. package/src/scene/blocks/code-diff/code-diff.html +817 -0
  182. package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
  183. package/src/scene/blocks/code-typing/code-typing.html +764 -0
  184. package/src/scene/blocks/data-chart/data-chart.html +452 -0
  185. package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
  186. package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
  187. package/src/scene/blocks/index.json +824 -0
  188. package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
  189. package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
  190. package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
  191. package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
  192. package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
  193. package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
  194. package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
  195. package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
  196. package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
  197. package/src/scene/blocks/mk-background/mk-background.html +236 -0
  198. package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
  199. package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
  200. package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
  201. package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
  202. package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
  203. package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
  204. package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
  205. package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
  206. package/src/scene/blocks/snow/snow.html +205 -0
  207. package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
  208. package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
  209. package/src/scene/blocks/starfield/starfield.html +188 -0
  210. package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
  211. package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
  212. package/src/scene/blocks/x-post/x-post.html +437 -0
  213. package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
  214. package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
  215. package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
  216. package/src/scene/blocks.ts +229 -0
  217. package/src/scene/command.ts +272 -0
  218. package/src/scene/figure.ts +181 -0
  219. package/src/scene/glyphs.ts +107 -0
  220. package/src/scene/init.ts +86 -0
  221. package/src/scene/inspect/css-tree.d.ts +6 -0
  222. package/src/scene/inspect/page-scripts.ts +272 -0
  223. package/src/scene/inspect/report.ts +405 -0
  224. package/src/scene/inspect/run.ts +200 -0
  225. package/src/scene/inspect/source.ts +236 -0
  226. package/src/scene/label.ts +131 -0
  227. package/src/scene/net-guard.ts +74 -0
  228. package/src/scene/render.ts +292 -0
  229. package/src/scene/runtime/browser.ts +199 -0
  230. package/src/scene/runtime/core.ts +181 -0
  231. package/src/scene/runtime/react.tsx +115 -0
  232. package/src/scene/runtime/transitions.ts +133 -0
  233. package/src/scene/sanitize.ts +181 -0
  234. package/src/scene/scene.ts +152 -0
  235. package/src/scene/studio-command.ts +48 -0
  236. package/src/scene/studio-skin.ts +59 -0
  237. package/src/scene/studio.ts +337 -0
  238. package/src/scene/supply.ts +111 -0
  239. package/src/scene/timing.ts +288 -0
  240. package/src/scene/transitions/LICENSE +190 -0
  241. package/src/scene/transitions/NOTICE +5 -0
  242. package/src/scene/transitions/SOURCE.json +24 -0
  243. package/src/scene/transitions/transitions.global.js +38 -0
  244. package/src/scene/transitions.ts +26 -0
  245. package/src/service/all-projects.ts +153 -0
  246. package/src/service/commands.ts +256 -0
  247. package/src/service/due.ts +82 -0
  248. package/src/service/lock.ts +101 -0
  249. package/src/service/log-file.ts +56 -0
  250. package/src/service/state.ts +41 -0
  251. package/src/service/units.ts +90 -0
  252. package/src/service/worker.ts +51 -0
  253. package/src/skill-agents.ts +74 -0
  254. package/src/skill-install.ts +334 -0
  255. package/src/sync-write.ts +21 -0
  256. package/src/tick-report.ts +205 -0
  257. package/src/tts.ts +124 -0
  258. package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
  259. package/vendor/@vfrey/contracts/dist/account.js +27 -0
  260. package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
  261. package/vendor/@vfrey/contracts/dist/checks.js +1 -0
  262. package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
  263. package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
  264. package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
  265. package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
  266. package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
  267. package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
  268. package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
  269. package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
  270. package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
  271. package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
  272. package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
  273. package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
  274. package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
  275. package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
  276. package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
  277. package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
  278. package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
  279. package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
  280. package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
  281. package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
  282. package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
  283. package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
  284. package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
  285. package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
  286. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
  287. package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
  288. package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
  289. package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
  290. package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
  291. package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
  292. package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
  293. package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
  294. package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
  295. package/vendor/@vfrey/contracts/dist/events.js +1 -0
  296. package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
  297. package/vendor/@vfrey/contracts/dist/http.js +37 -0
  298. package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
  299. package/vendor/@vfrey/contracts/dist/index.js +22 -0
  300. package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
  301. package/vendor/@vfrey/contracts/dist/log.js +10 -0
  302. package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
  303. package/vendor/@vfrey/contracts/dist/mail.js +1 -0
  304. package/vendor/@vfrey/contracts/package.json +18 -0
  305. package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
  306. package/vendor/@vfrey/ingest/dist/index.js +221 -0
  307. package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
  308. package/vendor/@vfrey/ingest/dist/parse.js +165 -0
  309. package/vendor/@vfrey/ingest/package.json +21 -0
  310. package/vendor/framey/LICENSE +190 -0
  311. package/vendor/framey/NOTICE +15 -0
  312. package/vendor/framey/SOURCE.json +24 -0
  313. package/vendor/framey/STUDIO-SOURCE.json +9 -0
  314. package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
  315. package/vendor/framey/bin/framey.mjs +16 -0
  316. package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
  317. package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
  318. package/vendor/framey/dist/api.d.ts +211 -0
  319. package/vendor/framey/dist/api.js +76 -0
  320. package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
  321. package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
  322. package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
  323. package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
  324. package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
  325. package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
  326. package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
  327. package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
  328. package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
  329. package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
  330. package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
  331. package/vendor/framey/dist/check-F5N544ML.js +111 -0
  332. package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
  333. package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
  334. package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
  335. package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
  336. package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
  337. package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
  338. package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
  339. package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
  340. package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
  341. package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
  342. package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
  343. package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
  344. package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
  345. package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
  346. package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
  347. package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
  348. package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
  349. package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
  350. package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
  351. package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
  352. package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
  353. package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
  354. package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
  355. package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
  356. package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
  357. package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
  358. package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
  359. package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
  360. package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
  361. package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
  362. package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
  363. package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
  364. package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
  365. package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
  366. package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
  367. package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
  368. package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
  369. package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
  370. package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
  371. package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
  372. package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
  373. package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
  374. package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
  375. package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
  376. package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
  377. package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
  378. package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
  379. package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
  380. package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
  381. package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
  382. package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
  383. package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
  384. package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
  385. package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
  386. package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
  387. package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
  388. package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
  389. package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
  390. package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
  391. package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
  392. package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
  393. package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
  394. package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
  395. package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
  396. package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
  397. package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
  398. package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
  399. package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
  400. package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
  401. package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
  402. package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
  403. package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
  404. package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
  405. package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
  406. package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
  407. package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
  408. package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
  409. package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
  410. package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
  411. package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
  412. package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
  413. package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
  414. package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
  415. package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
  416. package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
  417. package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
  418. package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
  419. package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
  420. package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
  421. package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
  422. package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
  423. package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
  424. package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
  425. package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
  426. package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
  427. package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
  428. package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
  429. package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
  430. package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
  431. package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
  432. package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
  433. package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
  434. package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
  435. package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
  436. package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
  437. package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
  438. package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
  439. package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
  440. package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
  441. package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
  442. package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
  443. package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
  444. package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
  445. package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
  446. package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
  447. package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
  448. package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
  449. package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
  450. package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
  451. package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
  452. package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
  453. package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
  454. package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
  455. package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
  456. package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
  457. package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
  458. package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
  459. package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
  460. package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
  461. package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
  462. package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
  463. package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
  464. package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
  465. package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
  466. package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
  467. package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
  468. package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
  469. package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
  470. package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
  471. package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
  472. package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
  473. package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
  474. package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
  475. package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
  476. package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
  477. package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
  478. package/vendor/framey/dist/cli.js +439 -0
  479. package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
  480. package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
  481. package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
  482. package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
  483. package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
  484. package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
  485. package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
  486. package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
  487. package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
  488. package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
  489. package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
  490. package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
  491. package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
  492. package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
  493. package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
  494. package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
  495. package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
  496. package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
  497. package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
  498. package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
  499. package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
  500. package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
  501. package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
  502. package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
  503. package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
  504. package/vendor/framey/dist/fontLocalizeCli.js +142 -0
  505. package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
  506. package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
  507. package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
  508. package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
  509. package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
  510. package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
  511. package/vendor/framey/dist/hyperframe.manifest.json +22 -0
  512. package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
  513. package/vendor/framey/dist/hyperframes-player.global.js +478 -0
  514. package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
  515. package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
  516. package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
  517. package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
  518. package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
  519. package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
  520. package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
  521. package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
  522. package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
  523. package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
  524. package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
  525. package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
  526. package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
  527. package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
  528. package/vendor/framey/dist/models-K363FYM4.js +195 -0
  529. package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
  530. package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
  531. package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
  532. package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
  533. package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
  534. package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
  535. package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
  536. package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
  537. package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
  538. package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
  539. package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
  540. package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
  541. package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
  542. package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
  543. package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
  544. package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
  545. package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
  546. package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
  547. package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
  548. package/vendor/framey/dist/renderSetupWorker.js +120 -0
  549. package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
  550. package/vendor/framey/dist/runtimeVersion.js +18 -0
  551. package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
  552. package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
  553. package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
  554. package/vendor/framey/dist/sherpaWorker.js +84 -0
  555. package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
  556. package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
  557. package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
  558. package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
  559. package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
  560. package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
  561. package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
  562. package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
  563. package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
  564. package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
  565. package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
  566. package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
  567. package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
  568. package/vendor/framey/dist/studio/favicon.svg +14 -0
  569. package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
  570. package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
  571. package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
  572. package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
  573. package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
  574. package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
  575. package/vendor/framey/dist/studio/index.html +23 -0
  576. package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
  577. package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
  578. package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
  579. package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
  580. package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
  581. package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
  582. package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
  583. package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
  584. package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
  585. package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
  586. package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
  587. package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
  588. package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
  589. package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
  590. package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
  591. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
  592. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
  593. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
  594. package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
  595. package/vendor/framey/dist/warmCheck.js +220 -0
  596. package/vendor/framey/dist/warmCheckClient.js +139 -0
  597. package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
  598. package/vendor/framey-player/LICENSE +190 -0
  599. package/vendor/framey-player/NOTICE +16 -0
  600. package/vendor/framey-player/SOURCE.json +6 -0
  601. package/vendor/framey-player/hyperframes-player.global.js +478 -0
  602. package/vendor/framey.pin +1 -0
  603. package/bin/index.js +0 -3
@@ -0,0 +1,292 @@
1
+ import { cp, mkdir, readdir, readFile, rename, rm, stat, writeFile } from "node:fs/promises";
2
+ import { createRequire } from "node:module";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import type { WebpackOverrideFn } from "@remotion/bundler";
7
+ import { commandFor, FRAMEY_ENTRY, hyperframesCli } from "../product/probes.js";
8
+ import { ensureChrome, run } from "../render/html-engine.js";
9
+ import { probeVideo, type RenderedVideo } from "../render/probe.js";
10
+ import { withJsExtensions } from "../render/webpack.js";
11
+ import { sceneTimingFor } from "./audio.js";
12
+ import { burnLabel, burnLabelStill } from "./label.js";
13
+ import { startNetGuard } from "./net-guard.js";
14
+ import { SceneError } from "./scene.js";
15
+ import { SUPPLY_DIR, writeSupply } from "./supply.js";
16
+ import type { BeatInput, SceneTiming } from "./timing.js";
17
+
18
+ type Env = Record<string, string | undefined>;
19
+
20
+ export type SceneEngine = "vfrey-framey" | "hyperframes" | "remotion";
21
+
22
+ export const SCENE_FRAME = { width: 1080, height: 1920, fps: 30 } as const;
23
+
24
+ export interface SceneRenderInput {
25
+ engine: SceneEngine;
26
+ /** The scene folder (vfrey's stored copy of it). It is never written to. */
27
+ sceneDir: string;
28
+ title: string;
29
+ /** The spoken lines, in order. */
30
+ lines: string[];
31
+ beats: BeatInput[];
32
+ voiceoverPath: string | null;
33
+ /** The mp4 to write, or the image when `frameAt` is set. */
34
+ outputPath: string;
35
+ /** A folder this render owns. It is removed when the render ends, good or bad. */
36
+ workDir: string;
37
+ /** Render one frame at this time (seconds) instead of the whole video. */
38
+ frameAt?: number;
39
+ env?: Env;
40
+ cwd?: string;
41
+ /** Called for each thing worth telling the person: a blocked network request, a rewritten script tag. */
42
+ warn?: (message: string) => void;
43
+ }
44
+
45
+ export interface SceneRenderResult {
46
+ timing: SceneTiming;
47
+ /** The real length of the written video, from ffprobe. Null for a single frame. */
48
+ durationSec: number | null;
49
+ /** What ffprobe says the file is (the same probe the other engines record). */
50
+ rendered: RenderedVideo | null;
51
+ }
52
+
53
+ export const GSAP_CDN = /<script\b[^>]*\bsrc\s*=\s*["']https?:\/\/(?:cdn\.jsdelivr\.net\/npm\/gsap@[^"']*|cdnjs\.cloudflare\.com\/ajax\/libs\/gsap\/[^"']*|unpkg\.com\/gsap[^"']*|code\.jquery\.com\/[^"']*gsap[^"']*)["'][^>]*>\s*<\/script>/gi;
54
+
55
+ const num = (value: number) => String(Math.round(value * 1000) / 1000);
56
+
57
+ /** How much of the scene after a `--frame` is rendered with it, so a transition under way at that frame is complete in the clip. */
58
+ export const TRANSITION_TAIL_SECONDS = 4;
59
+
60
+ /** True when the scene's HTML loads the shader-transitions runtime or calls `vfrey.transition`. */
61
+ export function usesTransitions(html: string): boolean {
62
+ return /\bvfrey\.transition\s*\(|vfrey\/transitions\.js/.test(html);
63
+ }
64
+
65
+ /**
66
+ * Framey decides how to capture a render (layered, with the shaders composited) by reading the page's inline scripts for
67
+ * `HyperShader.init(` or `__hf.transitions =` before it loads anything. `vfrey.transition` makes that call from
68
+ * vfrey.js, which the check cannot see, so a scene that uses it gets this one inline line. It only makes sure the field
69
+ * exists; the transitions runtime fills it in.
70
+ */
71
+ export const TRANSITIONS_MARKER = "<script>if (window.__hf) window.__hf.transitions = window.__hf.transitions || [];</script>";
72
+
73
+ /**
74
+ * The composition HTML as the renderer needs it: the root is 1080 by 1920 and lasts exactly `durationSec`, the voice
75
+ * is on the timeline, and a GSAP script tag that points at a CDN points at vfrey's local copy instead.
76
+ */
77
+ export function patchComposition(html: string, timing: Pick<SceneTiming, "durationSec">, hasVoice: boolean): { html: string; notes: string[] } {
78
+ const notes: string[] = [];
79
+ const root = /<([a-zA-Z][\w-]*)((?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)?\bdata-composition-id\s*=(?:\s+(?:[^>"']|"[^"]*"|'[^']*')*|[^>])*)>/.exec(html);
80
+ if (!root) {
81
+ throw new SceneError(
82
+ "index.html has no element with data-composition-id, so there is nothing to render.",
83
+ 'wrap the scene in <div id="root" data-composition-id="main">...</div>, as `npx vfrey scene init DIR` shows.',
84
+ "scene_no_root",
85
+ );
86
+ }
87
+ let attrs = root[2]!;
88
+ const set = (name: string, value: string) => {
89
+ const pattern = new RegExp(`\\s${name}\\s*=\\s*("[^"]*"|'[^']*'|[^\\s>]+)`, "i");
90
+ attrs = pattern.test(attrs) ? attrs.replace(pattern, ` ${name}="${value}"`) : `${attrs} ${name}="${value}"`;
91
+ };
92
+ set("data-start", "0");
93
+ set("data-duration", num(timing.durationSec));
94
+ set("data-width", String(SCENE_FRAME.width));
95
+ set("data-height", String(SCENE_FRAME.height));
96
+ const audio = hasVoice
97
+ ? `\n<audio id="vfrey-voice" src="${SUPPLY_DIR}/voice.wav" data-start="0" data-duration="${num(timing.durationSec)}" data-track-index="0" data-volume="1"></audio>`
98
+ : "";
99
+ let out = `${html.slice(0, root.index)}<${root[1]}${attrs}>${audio}${html.slice(root.index + root[0].length)}`;
100
+ if (usesTransitions(out) && !out.includes(TRANSITIONS_MARKER)) {
101
+ out = /<\/head>/i.test(out) ? out.replace(/<\/head>/i, `${TRANSITIONS_MARKER}\n</head>`) : `${TRANSITIONS_MARKER}\n${out}`;
102
+ }
103
+ out = out.replace(GSAP_CDN, () => {
104
+ notes.push(`The GSAP script tag that loads from a CDN now loads vfrey's local copy (${SUPPLY_DIR}/gsap.min.js).`);
105
+ return `<script src="${SUPPLY_DIR}/gsap.min.js"></script>`;
106
+ });
107
+ return { html: out, notes };
108
+ }
109
+
110
+ function blockedWarnings(hosts: string[]): string[] {
111
+ return hosts.map((host) => `The scene tried to reach ${host} over the network. The network is blocked while a scene renders, so the request failed.`);
112
+ }
113
+
114
+ async function chromeFor(env: Env, cwd: string): Promise<string> {
115
+ return ensureChrome(env, cwd);
116
+ }
117
+
118
+ /** Renders a scene (or one frame of it) with the chosen engine, then puts the AI label on it. */
119
+ export async function renderScene(input: SceneRenderInput): Promise<SceneRenderResult> {
120
+ const env = input.env ?? process.env;
121
+ const cwd = input.cwd ?? process.cwd();
122
+ await rm(input.workDir, { recursive: true, force: true });
123
+ await mkdir(input.workDir, { recursive: true });
124
+ const guardDir = `${input.workDir}.guard`;
125
+ let guard: Awaited<ReturnType<typeof startNetGuard>> | null = null;
126
+ try {
127
+ const timing = await sceneTimingFor({ title: input.title, lines: input.lines, beats: input.beats }, input.voiceoverPath, env);
128
+ const chrome = await chromeFor(env, cwd);
129
+ guard = await startNetGuard(chrome, guardDir);
130
+ await mkdir(path.dirname(input.outputPath), { recursive: true });
131
+ await rm(input.outputPath, { force: true });
132
+ if (input.engine === "remotion") await renderTsx(input, timing, guard.chrome, env, cwd);
133
+ else await renderHtml(input, timing, guard.chrome, env, cwd);
134
+ const info = await stat(input.outputPath).catch(() => null);
135
+ if (!info || info.size <= 0) throw new Error("the renderer wrote no output");
136
+ for (const message of blockedWarnings(guard.blocked())) input.warn?.(message);
137
+ const labelDir = path.join(input.workDir, "label");
138
+ if (input.frameAt === undefined) await burnLabel(input.outputPath, labelDir, env);
139
+ else await burnLabelStill(input.outputPath, labelDir, env);
140
+ const rendered = input.frameAt === undefined ? await probeVideo(input.outputPath, env) : null;
141
+ return { timing, rendered, durationSec: rendered?.durationSec ?? null };
142
+ } finally {
143
+ await guard?.close().catch(() => undefined);
144
+ await rm(guardDir, { recursive: true, force: true });
145
+ await rm(input.workDir, { recursive: true, force: true });
146
+ }
147
+ }
148
+
149
+ async function renderHtml(input: SceneRenderInput, timing: SceneTiming, chrome: string, env: Env, cwd: string): Promise<void> {
150
+ const project = path.join(input.workDir, "project");
151
+ await cp(input.sceneDir, project, { recursive: true });
152
+ await writeSupply({ dir: project, timing, voiceFile: input.voiceoverPath, kind: "html", env });
153
+ const indexFile = path.join(project, "index.html");
154
+ const originalHtml = await readFile(indexFile, "utf8");
155
+ const patched = patchComposition(originalHtml, timing, input.voiceoverPath !== null);
156
+ for (const note of patched.notes) input.warn?.(note);
157
+ await writeFile(indexFile, patched.html);
158
+ let command: string;
159
+ let args: string[];
160
+ if (input.engine === "vfrey-framey") {
161
+ command = process.execPath;
162
+ args = [FRAMEY_ENTRY];
163
+ } else {
164
+ const cli = hyperframesCli(env, cwd);
165
+ if (!cli) throw new Error("the HyperFrames CLI was not found");
166
+ ({ command, args } = commandFor(cli.bin));
167
+ }
168
+ const runEnv = {
169
+ ...env,
170
+ PRODUCER_HEADLESS_SHELL_PATH: chrome,
171
+ HYPERFRAMES_BROWSER_PATH: chrome,
172
+ HYPERFRAMES_NO_TELEMETRY: "1",
173
+ HYPERFRAMES_NO_UPDATE_CHECK: "1",
174
+ DO_NOT_TRACK: "1",
175
+ } as NodeJS.ProcessEnv;
176
+ if (input.frameAt !== undefined && usesTransitions(patched.html)) {
177
+ await renderTransitionFrame(input, project, timing, originalHtml, command, args, runEnv);
178
+ return;
179
+ }
180
+ if (input.frameAt !== undefined) {
181
+ const shots = path.join(input.workDir, "shots");
182
+ await run(command, [...args, "snapshot", project, "--at", String(input.frameAt), "--no-end", "--describe", "false", "-o", shots], runEnv, project);
183
+ const png = (await readdir(shots)).find((name) => name.toLowerCase().endsWith(".png"));
184
+ if (!png) throw new Error("the renderer wrote no frame");
185
+ await rename(path.join(shots, png), input.outputPath);
186
+ return;
187
+ }
188
+ await run(command, [...args, "render", project, "-o", input.outputPath, "--quality", "standard", "--fps", String(SCENE_FRAME.fps)], runEnv, project);
189
+ }
190
+
191
+ /**
192
+ * One frame of a scene with shader transitions. A snapshot would show the shots stacked, because the shaders are
193
+ * composited by the render pipeline, not by the page. So the scene is rendered, cut off a few seconds after the frame
194
+ * asked for, and that frame is taken from the clip. The tail matters: the pipeline composites a transition only when the
195
+ * clip lasts past its end (cut at the frame itself, a frame in the middle of a transition shows the target shot alone).
196
+ */
197
+ async function renderTransitionFrame(input: SceneRenderInput, project: string, timing: SceneTiming, html: string, command: string, args: string[], runEnv: NodeJS.ProcessEnv): Promise<void> {
198
+ const fps = SCENE_FRAME.fps;
199
+ const total = Math.max(1, Math.round(timing.durationSec * fps));
200
+ const index = Math.min(total - 1, Math.max(0, Math.round(input.frameAt! * fps)));
201
+ const clip = patchComposition(html, { durationSec: Math.min(timing.durationSec, (index + 1) / fps + TRANSITION_TAIL_SECONDS) }, false);
202
+ await writeFile(path.join(project, "index.html"), clip.html);
203
+ const video = path.join(input.workDir, "frame-clip.mp4");
204
+ await run(command, [...args, "render", project, "-o", video, "--quality", "standard", "--fps", String(fps)], runEnv, project);
205
+ await run("ffmpeg", ["-y", "-v", "error", "-i", video, "-vf", `select=eq(n\\,${index})`, "-frames:v", "1", input.outputPath], runEnv, project);
206
+ }
207
+
208
+ /** Where webpack finds `react`, `remotion` and the rest for a scene that lives outside the vfrey install. */
209
+ function resolveRoots(): { modules: string[]; alias: Record<string, string> } {
210
+ const require = createRequire(import.meta.url);
211
+ const alias: Record<string, string> = {};
212
+ for (const name of ["react", "react-dom", "remotion"]) alias[name] = path.dirname(require.resolve(`${name}/package.json`));
213
+ return { modules: require.resolve.paths("react") ?? [], alias };
214
+ }
215
+
216
+ export const SCENE_RUNTIME_ENTRY = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "runtime/react.tsx");
217
+
218
+ export function sceneWebpack(): WebpackOverrideFn {
219
+ const roots = resolveRoots();
220
+ return (current) => {
221
+ const base = withJsExtensions(current) as typeof current;
222
+ return {
223
+ ...base,
224
+ resolve: {
225
+ ...base.resolve,
226
+ modules: [...(base.resolve?.modules ?? ["node_modules"]), ...roots.modules],
227
+ alias: { ...(Array.isArray(base.resolve?.alias) ? {} : base.resolve?.alias), ...roots.alias, "@vfrey/scene": SCENE_RUNTIME_ENTRY },
228
+ },
229
+ };
230
+ };
231
+ }
232
+
233
+ /** The Remotion entry vfrey generates around the scene: the provider, the voice, and one composition. */
234
+ export function remotionEntry(timing: SceneTiming, hasVoice: boolean): string {
235
+ return `import React from "react";
236
+ import { AbsoluteFill, Audio, Composition, registerRoot, staticFile } from "remotion";
237
+ import { VfreyProvider } from "@vfrey/scene";
238
+ import * as sceneModule from "./scene/index";
239
+ import timing from "./scene/${SUPPLY_DIR}/timing.json";
240
+
241
+ const Scene: React.ComponentType = (sceneModule as any).default ?? (sceneModule as any).Scene;
242
+ if (!Scene) throw new Error("index.tsx must export a React component: export default function Scene() {...}");
243
+
244
+ function Wrapped() {
245
+ return (
246
+ <VfreyProvider timing={timing as any}>
247
+ ${hasVoice ? `<Audio src={staticFile("${SUPPLY_DIR}/voice.wav")} />` : ""}
248
+ <AbsoluteFill>
249
+ <Scene />
250
+ </AbsoluteFill>
251
+ </VfreyProvider>
252
+ );
253
+ }
254
+
255
+ registerRoot(() => (
256
+ <Composition id="Scene" component={Wrapped} durationInFrames={${Math.max(1, Math.round(timing.durationSec * SCENE_FRAME.fps))}} fps={${SCENE_FRAME.fps}} width={${SCENE_FRAME.width}} height={${SCENE_FRAME.height}} />
257
+ ));
258
+ `;
259
+ }
260
+
261
+ async function renderTsx(input: SceneRenderInput, timing: SceneTiming, chrome: string, env: Env, _cwd: string): Promise<void> {
262
+ const sceneCopy = path.join(input.workDir, "scene");
263
+ await cp(input.sceneDir, sceneCopy, { recursive: true });
264
+ await writeSupply({ dir: sceneCopy, timing, voiceFile: input.voiceoverPath, kind: "tsx", env });
265
+ const entry = path.join(input.workDir, "entry.tsx");
266
+ await writeFile(entry, remotionEntry(timing, input.voiceoverPath !== null));
267
+ const { bundle } = await import("@remotion/bundler");
268
+ const { renderMedia, renderStill, selectComposition } = await import("@remotion/renderer");
269
+ const serveUrl = await bundle({
270
+ entryPoint: entry,
271
+ publicDir: sceneCopy,
272
+ outDir: path.join(input.workDir, "bundle"),
273
+ enableCaching: false,
274
+ webpackOverride: sceneWebpack(),
275
+ });
276
+ const port = Number(env.VFREY_REMOTION_PORT) || undefined;
277
+ const common = { serveUrl, browserExecutable: chrome, ...(port ? { port } : {}) };
278
+ const composition = await selectComposition({ ...common, id: "Scene", inputProps: {} });
279
+ if (input.frameAt !== undefined) {
280
+ const frame = Math.min(composition.durationInFrames - 1, Math.max(0, Math.round(input.frameAt * SCENE_FRAME.fps)));
281
+ await renderStill({ ...common, composition, frame, output: input.outputPath, imageFormat: "png", inputProps: {} });
282
+ return;
283
+ }
284
+ await renderMedia({
285
+ ...common,
286
+ composition,
287
+ codec: "h264",
288
+ outputLocation: input.outputPath,
289
+ inputProps: {},
290
+ concurrency: Math.max(1, Math.min(4, os.availableParallelism())),
291
+ });
292
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * `vfrey/vfrey.js`: the helper an HTML scene includes with one script tag. It is bundled to a classic script at
3
+ * render time, with the timing file embedded, so `vfrey.timing` is there the moment the script has run.
4
+ */
5
+ import { plan, type TransitionSpec } from "./transitions.js";
6
+ import { captionCss, captionGroupHtml, captionGroups, lineView, wordAt, type CaptionOptions, type SceneTiming, type TimingWord } from "./core.js";
7
+
8
+ interface Timeline {
9
+ set(target: unknown, vars: Record<string, unknown>, position?: number): Timeline;
10
+ [key: string]: unknown;
11
+ }
12
+ interface Gsap {
13
+ timeline(vars?: Record<string, unknown>): Timeline;
14
+ }
15
+
16
+ type Cue<T> = (tl: any, t: number, subject: T) => void;
17
+
18
+ const g = globalThis as unknown as {
19
+ gsap?: Gsap;
20
+ document: Document;
21
+ __timelines?: Record<string, Timeline>;
22
+ __vfreyTiming?: SceneTiming;
23
+ vfrey?: unknown;
24
+ HyperShader?: { init(config: Record<string, unknown>): Timeline };
25
+ };
26
+
27
+ const timing = g.__vfreyTiming as SceneTiming;
28
+ if (!timing) throw new Error("vfrey: the timing data is missing. Is vfrey/vfrey.js the file vfrey wrote at render time?");
29
+
30
+ let current: Timeline | null = null;
31
+ let captionBlocks = 0;
32
+
33
+ interface TransitionCall extends TransitionSpec {
34
+ from: HTMLElement;
35
+ to: HTMLElement;
36
+ names: [string, string];
37
+ accent?: string;
38
+ background?: string;
39
+ }
40
+ const transitionCalls: TransitionCall[] = [];
41
+ let transitionsScheduled = false;
42
+ let shotIds = 0;
43
+
44
+ /** Wires every `vfrey.transition` call into Framey's shader runtime, once, when the page's scripts have all run. */
45
+ function applyTransitions(): void {
46
+ const shaders = g.HyperShader;
47
+ if (!shaders) throw new Error('vfrey.transition: the transitions runtime is not loaded. Add <script src="vfrey/transitions.js"></script> before <script src="vfrey/vfrey.js"></script>.');
48
+ const names = new Map<HTMLElement, string>();
49
+ for (const call of transitionCalls) {
50
+ names.set(call.from, call.names[0]);
51
+ names.set(call.to, call.names[1]);
52
+ }
53
+ const chain = plan(transitionCalls, (el: HTMLElement) => names.get(el) ?? "a shot");
54
+ if (!g.document.getElementById("vfrey-shots-css")) {
55
+ const css = g.document.createElement("style");
56
+ css.id = "vfrey-shots-css";
57
+ // Zero specificity: a scene's own CSS for its shots wins. Shots stack on top of each other in the frame.
58
+ css.textContent = ":where(.scene){position:absolute;left:0;top:0;width:100%;height:100%;}";
59
+ g.document.head.appendChild(css);
60
+ }
61
+ for (const shot of chain.scenes) {
62
+ if (!shot.id) shot.id = `vf-shot-${(shotIds += 1)}`;
63
+ shot.classList.add("scene");
64
+ }
65
+ const options = transitionCalls.find((c) => c.accent || c.background);
66
+ shaders.init({
67
+ bgColor: options?.background ?? "#000000",
68
+ ...(options?.accent ? { accentColor: options.accent } : {}),
69
+ scenes: chain.scenes.map((shot) => shot.id),
70
+ transitions: chain.transitions,
71
+ timeline: pick(),
72
+ });
73
+ }
74
+
75
+ function rootId(): string {
76
+ return g.document.querySelector("[data-composition-id]")?.getAttribute("data-composition-id") ?? "main";
77
+ }
78
+
79
+ /** Makes the paused GSAP timeline the scene animates on, and registers it where the renderer looks for it. */
80
+ function makeTimeline(): Timeline {
81
+ if (!g.gsap) throw new Error('vfrey: GSAP is not loaded. Add <script src="vfrey/gsap.min.js"></script> before <script src="vfrey/vfrey.js"></script>.');
82
+ const tl = g.gsap.timeline({ paused: true });
83
+ g.__timelines = g.__timelines || {};
84
+ g.__timelines[rootId()] = tl;
85
+ current = tl;
86
+ return tl;
87
+ }
88
+
89
+ function pick(tl?: Timeline): Timeline {
90
+ return tl ?? current ?? makeTimeline();
91
+ }
92
+
93
+ function element(target: string | Element): HTMLElement {
94
+ const found = typeof target === "string" ? g.document.querySelector(target) : target;
95
+ if (!found) throw new Error(`vfrey.captions: nothing matches ${String(target)}.`);
96
+ return found as HTMLElement;
97
+ }
98
+
99
+ const vfrey = {
100
+ /** Words, lines and beats with start and end times in seconds, and `durationSec`. The same data as vfrey/timing.json. */
101
+ timing,
102
+ /** Creates the scene's paused GSAP timeline and registers it. Call it once; `at`, `onWord` and `captions` add to it. */
103
+ timeline: makeTimeline,
104
+ /** Uses a timeline you made yourself as the one `at`, `onWord` and `captions` add to. */
105
+ use(tl: Timeline): Timeline {
106
+ current = tl;
107
+ return tl;
108
+ },
109
+ /** `vfrey.at(2.5, (tl, t) => tl.from("#x", { opacity: 0, duration: 0.3 }, t))`. Runs `fn` while the timeline is built, so a frame is the same on every render. */
110
+ at(t: number, fn: Cue<number>, tl?: Timeline): void {
111
+ if (!Number.isFinite(t) || t < 0) throw new Error(`vfrey.at: ${String(t)} is not a time in seconds.`);
112
+ fn(pick(tl), t, t);
113
+ },
114
+ /**
115
+ * Times a block from `npx vfrey scene add`: the block mounted on `target` (a selector or an element) starts at `start`
116
+ * seconds, and lasts `duration` seconds when given. `vfrey.onLine(1, (tl, t) => vfrey.block("#block-lt-clean-bar", t))`.
117
+ */
118
+ block(target: string | Element, start: number, duration?: number): void {
119
+ if (!Number.isFinite(start) || start < 0) throw new Error(`vfrey.block: ${String(start)} is not a time in seconds.`);
120
+ const found = typeof target === "string" ? g.document.querySelector(target) : target;
121
+ if (!found) throw new Error(`vfrey.block: nothing matches ${String(target)}. Paste the snippet \`npx vfrey scene add\` printed into your root element first.`);
122
+ found.setAttribute("data-start", String(Math.round(start * 1000) / 1000));
123
+ if (duration !== undefined) found.setAttribute("data-duration", String(duration));
124
+ },
125
+ /**
126
+ * A shader transition between two shots (elements that fill the frame, stacked on top of each other): at `at`
127
+ * seconds, for `duration` (default 0.7), `from` becomes `to`. `type` is one of `npx vfrey scene transitions` (default
128
+ * cross-warp-morph; `crossfade` needs no GPU). Calls chain: each `from` is the previous call's `to`, and they may be
129
+ * made in any order. Seekable and deterministic: the shots' opacity is on the scene's GSAP timeline, and the renderer
130
+ * composites the shader per frame. Needs `<script src="vfrey/transitions.js">` first. Options: ease, accent, background (hex).
131
+ * `vfrey.transition("#intro", "#demo", { type: "glitch", at: 3.2, duration: 0.6 })`.
132
+ */
133
+ transition(from: string | Element, to: string | Element, options: Omit<TransitionSpec, "from" | "to"> & { accent?: string; background?: string }): void {
134
+ const find = (target: string | Element, role: string): HTMLElement => {
135
+ const found = typeof target === "string" ? g.document.querySelector(target) : target;
136
+ if (!found) throw new Error(`vfrey.transition: nothing matches ${String(target)} (the ${role} shot).`);
137
+ return found as HTMLElement;
138
+ };
139
+ if (!options || typeof options !== "object") throw new Error('vfrey.transition: pass { type, at, duration }, for example { type: "glitch", at: 3.2 }.');
140
+ const label = (t: string | Element, el: HTMLElement) => (typeof t === "string" ? t : el.id ? `#${el.id}` : el.tagName.toLowerCase());
141
+ const a = find(from, "from");
142
+ const b = find(to, "to");
143
+ transitionCalls.push({ ...options, from: a, to: b, names: [label(from, a), label(to, b)] });
144
+ if (!transitionsScheduled) {
145
+ transitionsScheduled = true;
146
+ if (g.document.readyState === "loading") g.document.addEventListener("DOMContentLoaded", applyTransitions);
147
+ else Promise.resolve().then(applyTransitions);
148
+ }
149
+ },
150
+ /** Cues `fn` at the moment word `i` starts being spoken. */
151
+ onWord(i: number, fn: Cue<TimingWord>, tl?: Timeline): void {
152
+ const word = wordAt(timing, i);
153
+ fn(pick(tl), word.start, word);
154
+ },
155
+ /** Cues `fn` at the moment line `i` starts being spoken. */
156
+ onLine(i: number, fn: Cue<ReturnType<typeof lineView>>, tl?: Timeline): void {
157
+ const line = lineView(timing, i);
158
+ fn(pick(tl), line.start, line);
159
+ },
160
+ /** One spoken line: `{ i, text, start, end, words }`. */
161
+ line: (i: number) => lineView(timing, i),
162
+ word: (i: number) => wordAt(timing, i),
163
+ beat(i: number) {
164
+ const beat = timing.beats[i];
165
+ if (!beat) throw new Error(`vfrey: there is no beat ${i}; the script has ${timing.beats.length} beats.`);
166
+ return beat;
167
+ },
168
+ /**
169
+ * Word-highlighted captions in `el` (a selector or an element): groups of a few words, the spoken word marked.
170
+ * Position the element anywhere. Options: style box|color|underline|pop|plain, size, color, highlight,
171
+ * highlightText, font, maxWords, maxChars, align, stroke. Returns the groups it made.
172
+ */
173
+ captions(target: string | Element, options: CaptionOptions & { timeline?: Timeline } = {}) {
174
+ const root = element(target);
175
+ const tl = pick(options.timeline);
176
+ captionBlocks += 1;
177
+ const id = `vf${captionBlocks}`;
178
+ const scope = `.vf-cap-${captionBlocks}`;
179
+ const css = g.document.createElement("style");
180
+ css.textContent = captionCss(options, scope);
181
+ g.document.head.appendChild(css);
182
+ root.classList.add("vf-captions", `vf-cap-${captionBlocks}`);
183
+ const groups = captionGroups(timing, options);
184
+ root.innerHTML = groups.map((group, i) => captionGroupHtml(group, i, id)).join("");
185
+ groups.forEach((group, i) => {
186
+ const box = g.document.getElementById(`${id}-g-${i}`)!;
187
+ tl.set(box, { className: "vf-grp on" }, group.start);
188
+ group.words.forEach((word, k) => {
189
+ const span = g.document.getElementById(`${id}-w-${i}-${k}`)!;
190
+ tl.set(span, { className: "vf-w on" }, word.start);
191
+ tl.set(span, { className: "vf-w" }, word.until);
192
+ });
193
+ tl.set(box, { className: "vf-grp" }, group.end);
194
+ });
195
+ return groups;
196
+ },
197
+ };
198
+
199
+ g.vfrey = vfrey;
@@ -0,0 +1,181 @@
1
+ /**
2
+ * The scene helpers both runtimes share: word groups for captions, the caption stylesheet, and timing lookups.
3
+ * No DOM and no Node here, so the same code runs in the browser helper (`vfrey.js`) and in React (`@vfrey/scene`).
4
+ */
5
+ import type { SceneTiming, TimingLine, TimingWord } from "../timing.js";
6
+
7
+ export type { SceneTiming, TimingLine, TimingWord };
8
+
9
+ /** Longest caption group, in characters, that fits two lines at the default size. */
10
+ export const GROUP_MAX_CHARS = 30;
11
+
12
+ /**
13
+ * Groups of about three to six words, ending at punctuation when that is possible. A small dynamic program: each
14
+ * cut costs more the further its size is from four, and a cut after a word with punctuation is free.
15
+ */
16
+ export function groupSizes(words: string[], maxWords = 6, maxChars = GROUP_MAX_CHARS): number[] {
17
+ const n = words.length;
18
+ if (n === 0) return [];
19
+ const best: number[] = new Array(n + 1).fill(Infinity);
20
+ const cut: number[] = new Array(n + 1).fill(0);
21
+ best[0] = 0;
22
+ for (let end = 1; end <= n; end += 1) {
23
+ for (let size = 1; size <= maxWords && size <= end; size += 1) {
24
+ const slice = words.slice(end - size, end);
25
+ const chars = slice.join(" ").length;
26
+ if (chars > maxChars && size > 1) continue;
27
+ let c = best[end - size]! + Math.abs(size - 4) * 0.6 + (size < 3 ? 2.5 : 0);
28
+ if (end < n && !/[,.;:!?—]$/.test(words[end - 1]!)) c += 0.7;
29
+ if (c < best[end]!) {
30
+ best[end] = c;
31
+ cut[end] = size;
32
+ }
33
+ }
34
+ }
35
+ const sizes: number[] = [];
36
+ for (let end = n; end > 0; end -= cut[end]!) sizes.unshift(cut[end]!);
37
+ return sizes;
38
+ }
39
+
40
+ export const CAPTION_STYLES = ["box", "color", "underline", "pop", "plain"] as const;
41
+ export type CaptionStyle = (typeof CAPTION_STYLES)[number];
42
+
43
+ export interface CaptionOptions {
44
+ /** `box` (default): the spoken word sits in a filled box. `color`: it changes color. `underline`: a bar under it. `pop`: it grows and changes color. `plain`: no highlight. */
45
+ style?: CaptionStyle;
46
+ /** Font size in px. Default 68. */
47
+ size?: number;
48
+ /** Text color. Default #000000. */
49
+ color?: string;
50
+ /** Box, color or underline color of the spoken word. Default #2196F3. */
51
+ highlight?: string;
52
+ /** Text color of the spoken word inside a box. Default #FFFFFF. */
53
+ highlightText?: string;
54
+ /** CSS font-family. Default "Titan One", which `vfrey/fonts/fonts.css` provides. */
55
+ font?: string;
56
+ /** Most words in one group. Default 6. */
57
+ maxWords?: number;
58
+ /** Most characters in one group. Default 30. */
59
+ maxChars?: number;
60
+ /** `center` (default), `left` or `right`. */
61
+ align?: "center" | "left" | "right";
62
+ /** Add a thick outline of this color around the text, for captions over busy pictures. */
63
+ stroke?: string;
64
+ }
65
+
66
+ export interface CaptionWord {
67
+ /** Index in `timing.words`. */
68
+ index: number;
69
+ text: string;
70
+ start: number;
71
+ /** Until the next word starts (or the group ends). */
72
+ until: number;
73
+ }
74
+
75
+ export interface CaptionGroup {
76
+ line: number;
77
+ words: CaptionWord[];
78
+ /** The group shows from `start` to `end`. */
79
+ start: number;
80
+ end: number;
81
+ }
82
+
83
+ /** The caption groups for a timing file: never across a line, each shown until the next begins or half a second after its last word. */
84
+ export function captionGroups(timing: Pick<SceneTiming, "words" | "lines">, options: CaptionOptions = {}): CaptionGroup[] {
85
+ const groups: CaptionGroup[] = [];
86
+ for (const line of timing.lines) {
87
+ const words = timing.words.slice(line.firstWord, line.lastWord + 1);
88
+ const sizes = groupSizes(words.map((w) => w.text), options.maxWords ?? 6, options.maxChars ?? GROUP_MAX_CHARS);
89
+ let at = 0;
90
+ for (const size of sizes) {
91
+ const slice = words.slice(at, at + size);
92
+ at += size;
93
+ groups.push({
94
+ line: line.i,
95
+ words: slice.map((w) => ({ index: w.i, text: w.text, start: w.start, until: w.end })),
96
+ start: slice[0]!.start,
97
+ end: 0,
98
+ });
99
+ }
100
+ }
101
+ groups.forEach((g, i) => {
102
+ const next = groups[i + 1];
103
+ const lastEnd = g.words[g.words.length - 1]!.until;
104
+ g.end = Math.min(next ? next.start : Infinity, lastEnd + 0.5);
105
+ g.words.forEach((w, k) => {
106
+ const after = g.words[k + 1];
107
+ w.until = after ? after.start : g.end;
108
+ });
109
+ });
110
+ return groups;
111
+ }
112
+
113
+ /** The group on screen at time t and the spoken word inside it, or -1 for none. */
114
+ export function captionStateAt(groups: CaptionGroup[], t: number): { group: number; word: number } {
115
+ for (let i = 0; i < groups.length; i += 1) {
116
+ const g = groups[i]!;
117
+ if (t >= g.start && t < g.end) {
118
+ let word = -1;
119
+ for (let k = 0; k < g.words.length; k += 1) if (g.words[k]!.start <= t) word = k;
120
+ return { group: i, word };
121
+ }
122
+ }
123
+ return { group: -1, word: -1 };
124
+ }
125
+
126
+ /** The stylesheet for `.vf-captions`. */
127
+ export function captionCss(options: CaptionOptions = {}, scope = ".vf-captions"): string {
128
+ const style = options.style ?? "box";
129
+ const size = options.size ?? 68;
130
+ const color = options.color ?? "#000000";
131
+ const hi = options.highlight ?? "#2196F3";
132
+ const hiText = options.highlightText ?? "#FFFFFF";
133
+ const font = options.font ?? '"Titan One", "Arial Black", sans-serif';
134
+ const justify = options.align === "left" ? "flex-start" : options.align === "right" ? "flex-end" : "center";
135
+ const outline = options.stroke ? `-webkit-text-stroke: ${Math.max(4, Math.round(size / 9))}px ${options.stroke}; paint-order: stroke fill;` : "";
136
+ const on =
137
+ style === "box"
138
+ ? `background: ${hi}; color: ${hiText};`
139
+ : style === "color"
140
+ ? `color: ${hi};`
141
+ : style === "underline"
142
+ ? `box-shadow: inset 0 -0.14em 0 ${hi};`
143
+ : style === "pop"
144
+ ? `color: ${hi}; transform: scale(1.14);`
145
+ : "";
146
+ return `
147
+ ${scope} { display: flex; flex-direction: column; justify-content: flex-end; align-items: ${justify}; font-family: ${font}; font-size: ${size}px; line-height: 1.12; color: ${color}; ${outline} }
148
+ ${scope} * { box-sizing: border-box; }
149
+ ${scope} .vf-grp { display: none; flex-wrap: wrap; justify-content: ${justify}; align-items: center; gap: 8px 10px; }
150
+ ${scope} .vf-grp.on { display: flex; }
151
+ ${scope} .vf-w { display: inline-block; padding: 4px 16px 8px; border-radius: 14px; background: transparent; ${style === "pop" ? "transform-origin: center bottom;" : ""} }
152
+ ${scope} .vf-w.on { ${on} }`;
153
+ }
154
+
155
+ export function escapeHtml(text: string): string {
156
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
157
+ }
158
+
159
+ /** One caption group as markup. `ids` gives every group and word an id the timeline cues point at. */
160
+ export function captionGroupHtml(group: CaptionGroup, index: number, prefix: string): string {
161
+ const spans = group.words.map((w, k) => `<span id="${prefix}-w-${index}-${k}" class="vf-w">${escapeHtml(w.text)}</span>`).join("");
162
+ return `<div id="${prefix}-g-${index}" class="vf-grp">${spans}</div>`;
163
+ }
164
+
165
+ export function lineAt(timing: Pick<SceneTiming, "lines">, i: number): TimingLine {
166
+ const line = timing.lines[i];
167
+ if (!line) throw new Error(`vfrey: there is no line ${i}; the script has ${timing.lines.length} lines (0 to ${timing.lines.length - 1}).`);
168
+ return line;
169
+ }
170
+
171
+ export function wordAt(timing: Pick<SceneTiming, "words">, i: number): TimingWord {
172
+ const word = timing.words[i];
173
+ if (!word) throw new Error(`vfrey: there is no word ${i}; the script has ${timing.words.length} words (0 to ${timing.words.length - 1}).`);
174
+ return word;
175
+ }
176
+
177
+ /** A line with its words, the shape `vfrey.line(i)` returns. */
178
+ export function lineView(timing: Pick<SceneTiming, "lines" | "words">, i: number): TimingLine & { words: TimingWord[] } {
179
+ const line = lineAt(timing, i);
180
+ return { ...line, words: timing.words.slice(line.firstWord, line.lastWord + 1) };
181
+ }