@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,115 @@
1
+ /**
2
+ * `@vfrey/scene`: what a Remotion scene imports. `useTiming()` is the timing file, `<Captions/>` draws the
3
+ * word-highlighted captions, `<AtLine/>` shows its children while a line is spoken. vfrey wraps the scene in the
4
+ * provider and plays the voice, so the scene never does either.
5
+ */
6
+ import { createContext, useContext, useLayoutEffect, useState, type CSSProperties, type ReactNode } from "react";
7
+ import { continueRender, delayRender, staticFile, useCurrentFrame, useVideoConfig } from "remotion";
8
+ import { captionCss, captionGroups, captionStateAt, lineAt, type CaptionOptions, type CaptionStyle, type SceneTiming } from "./core.js";
9
+
10
+ export type { CaptionOptions, CaptionStyle, SceneTiming };
11
+ export type { TimingBeat, TimingLine, TimingWord } from "../timing.js";
12
+
13
+ const TimingContext = createContext<SceneTiming | null>(null);
14
+
15
+ const FONTS = [
16
+ { family: "Titan One", file: "titan-one.woff2", weight: 400 },
17
+ { family: "JetBrains Mono", file: "jetbrains-mono-medium.woff2", weight: 500 },
18
+ { family: "Google Sans Flex", file: "google-sans-flex-latin.woff2", weight: 400 },
19
+ ] as const;
20
+
21
+ /** The scene's timing: words, lines and beats with start and end times in seconds, and `durationSec`. */
22
+ export function useTiming(): SceneTiming {
23
+ const timing = useContext(TimingContext);
24
+ if (!timing) throw new Error("@vfrey/scene: useTiming() ran outside a vfrey scene. Render it with `npx vfrey render --scene DIR`.");
25
+ return timing;
26
+ }
27
+
28
+ /** The time of the current frame, in seconds. */
29
+ export function useTime(): number {
30
+ const frame = useCurrentFrame();
31
+ return frame / useVideoConfig().fps;
32
+ }
33
+
34
+ /** Wraps the scene: provides the timing and loads the vfrey fonts before the first frame. vfrey adds this itself. */
35
+ export function VfreyProvider(props: { timing: SceneTiming; children: ReactNode }) {
36
+ const [handle] = useState(() => delayRender("vfrey fonts"));
37
+ useLayoutEffect(() => {
38
+ let cancelled = false;
39
+ Promise.all(FONTS.map((f) => document.fonts.load(`40px "${f.family}"`)))
40
+ .catch(() => undefined)
41
+ .finally(() => {
42
+ if (!cancelled) continueRender(handle);
43
+ });
44
+ return () => {
45
+ cancelled = true;
46
+ };
47
+ }, [handle]);
48
+ const css = FONTS.map(
49
+ (f) => `@font-face { font-family: "${f.family}"; src: url("${staticFile(`vfrey/fonts/${f.file}`)}") format("woff2"); font-weight: ${f.weight}; font-style: normal; }`,
50
+ ).join("\n");
51
+ return (
52
+ <TimingContext.Provider value={props.timing}>
53
+ <style>{css}</style>
54
+ {props.children}
55
+ </TimingContext.Provider>
56
+ );
57
+ }
58
+
59
+ export interface CaptionsProps extends Omit<CaptionOptions, "style"> {
60
+ /** `box` (default), `color`, `underline`, `pop` or `plain`. */
61
+ captionStyle?: CaptionStyle;
62
+ /** CSS for the caption block: position it anywhere, for example `{ position: "absolute", left: 50, right: 50, bottom: 200, height: 250 }`. */
63
+ style?: CSSProperties;
64
+ }
65
+
66
+ /** Word-highlighted captions: groups of a few words, the spoken word marked. */
67
+ export function Captions(props: CaptionsProps) {
68
+ const timing = useTiming();
69
+ const t = useTime();
70
+ const { captionStyle, style, ...options } = props;
71
+ const opts: CaptionOptions = { ...options, style: captionStyle };
72
+ const groups = captionGroups(timing, opts);
73
+ const state = captionStateAt(groups, t);
74
+ const group = state.group >= 0 ? groups[state.group] : undefined;
75
+ return (
76
+ <div className="vf-captions vf-cap-r" style={style} data-vfrey-captions="true">
77
+ <style>{captionCss(opts, ".vf-cap-r")}</style>
78
+ {group ? (
79
+ <div className="vf-grp on">
80
+ {group.words.map((word, k) => (
81
+ <span key={word.index} className={k === state.word ? "vf-w on" : "vf-w"}>
82
+ {word.text}
83
+ </span>
84
+ ))}
85
+ </div>
86
+ ) : null}
87
+ </div>
88
+ );
89
+ }
90
+
91
+ export interface AtLineProps {
92
+ /** The line to show during. */
93
+ i: number;
94
+ /** Show through the end of this later line instead. */
95
+ to?: number;
96
+ /** Seconds to keep showing after the line ends. Default: until the next line starts (or the video ends). */
97
+ hold?: number;
98
+ /** Children, or a function that gets how far through the window we are (0 to 1) and the seconds since it began. */
99
+ children: ReactNode | ((progress: number, seconds: number) => ReactNode);
100
+ }
101
+
102
+ /** Shows its children while line `i` (through line `to`) is spoken. */
103
+ export function AtLine(props: AtLineProps) {
104
+ const timing = useTiming();
105
+ const t = useTime();
106
+ const first = lineAt(timing, props.i);
107
+ const last = lineAt(timing, props.to ?? props.i);
108
+ const next = timing.lines[last.i + 1];
109
+ const end = props.hold !== undefined ? last.end + props.hold : next ? next.start : timing.durationSec;
110
+ // The first line is shown from the start of the video, not from its first word.
111
+ const start = props.i === 0 ? 0 : first.start;
112
+ if (t < start || t >= end) return null;
113
+ const progress = (t - start) / Math.max(1e-6, end - start);
114
+ return <>{typeof props.children === "function" ? props.children(progress, t - start) : props.children}</>;
115
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The pure half of `vfrey.transition`: which transition types exist, and the plan that turns the calls a scene made
3
+ * into the chain Framey's shader runtime wants. No DOM, no clock. The browser helper (browser.ts) and the
4
+ * `scene transitions` command share it.
5
+ */
6
+
7
+ export interface TransitionType {
8
+ name: string;
9
+ /** What it looks like, in a few words. */
10
+ look: string;
11
+ /** Needs WebGL while rendering (`crossfade` does not: it is two opacity tweens). */
12
+ gpu: boolean;
13
+ }
14
+
15
+ /** The shader names are Framey's (src/scene/transitions/SOURCE.json lists them; a test keeps the two in step). */
16
+ export const TRANSITION_TYPES: readonly TransitionType[] = [
17
+ { name: "crossfade", look: "plain dissolve, no shader", gpu: false },
18
+ { name: "cross-warp-morph", look: "noise-driven morph that blends both shots", gpu: true },
19
+ { name: "domain-warp", look: "organic noise warp with a glowing edge", gpu: true },
20
+ { name: "ridged-burn", look: "ridged-noise burn with sparks and heat glow", gpu: true },
21
+ { name: "whip-pan", look: "horizontal motion blur, a fast camera pan", gpu: true },
22
+ { name: "sdf-iris", look: "circular iris wipe with a glowing ring", gpu: true },
23
+ { name: "ripple-waves", look: "concentric ripples from the center", gpu: true },
24
+ { name: "gravitational-lens", look: "gravity-well warp with chromatic aberration", gpu: true },
25
+ { name: "cinematic-zoom", look: "radial zoom blur with colored fringing", gpu: true },
26
+ { name: "chromatic-split", look: "RGB channels separate from the center", gpu: true },
27
+ { name: "glitch", look: "digital glitch: block displacement and scanlines", gpu: true },
28
+ { name: "swirl-vortex", look: "spiral rotation with a noisy warp", gpu: true },
29
+ { name: "thermal-distortion", look: "heat shimmer rising from the bottom", gpu: true },
30
+ { name: "flash-through-white", look: "flash to white, then the next shot", gpu: true },
31
+ { name: "light-leak", look: "warm light leak with a lens flare", gpu: true },
32
+ ];
33
+
34
+ export const DEFAULT_TRANSITION = "cross-warp-morph";
35
+ export const DEFAULT_TRANSITION_SECONDS = 0.7;
36
+ export const DEFAULT_TRANSITION_EASE = "power2.inOut";
37
+
38
+ export interface TransitionSpec {
39
+ /** Anything that names a shot: a selector, an id, an element. Compared with `===`. */
40
+ from: unknown;
41
+ to: unknown;
42
+ type?: string;
43
+ /** Start, in seconds. */
44
+ at: number;
45
+ duration?: number;
46
+ ease?: string;
47
+ }
48
+
49
+ export interface PlannedTransition {
50
+ time: number;
51
+ /** Undefined for `crossfade`. */
52
+ shader: string | undefined;
53
+ duration: number;
54
+ ease: string;
55
+ }
56
+
57
+ export interface TransitionPlan<T> {
58
+ /** The shots in order: `transitions.length + 1` of them. */
59
+ scenes: T[];
60
+ transitions: PlannedTransition[];
61
+ }
62
+
63
+ const round = (n: number) => Math.round(n * 1000) / 1000;
64
+
65
+ function closest(name: string): string | null {
66
+ const lower = name.toLowerCase();
67
+ const hit = TRANSITION_TYPES.find((t) => t.name.includes(lower) || lower.includes(t.name));
68
+ return hit?.name ?? null;
69
+ }
70
+
71
+ /** Throws a plain Error that says what is wrong and what to write instead. */
72
+ export function plan<T>(specs: ReadonlyArray<TransitionSpec & { from: T; to: T }>, label: (shot: T) => string = String): TransitionPlan<T> {
73
+ if (specs.length === 0) throw new Error("vfrey.transition: no transitions to plan.");
74
+ const names = TRANSITION_TYPES.map((t) => t.name);
75
+ const sorted = specs
76
+ .map((spec, index) => {
77
+ const type = spec.type ?? DEFAULT_TRANSITION;
78
+ if (!names.includes(type)) {
79
+ const near = closest(type);
80
+ throw new Error(`vfrey.transition: "${type}" is not a transition type.${near ? ` Did you mean "${near}"?` : ""} Types: ${names.join(", ")} (npx vfrey scene transitions).`);
81
+ }
82
+ if (!Number.isFinite(spec.at) || spec.at < 0) throw new Error(`vfrey.transition: at must be a time in seconds, not ${String(spec.at)}.`);
83
+ const duration = spec.duration ?? DEFAULT_TRANSITION_SECONDS;
84
+ if (!Number.isFinite(duration) || duration <= 0) throw new Error(`vfrey.transition: duration must be more than 0 seconds, not ${String(spec.duration)}.`);
85
+ if (spec.from === spec.to) throw new Error(`vfrey.transition: a transition needs two different shots, and both are ${label(spec.from)}.`);
86
+ return { spec, index, type, duration };
87
+ })
88
+ .sort((a, b) => a.spec.at - b.spec.at || a.index - b.index);
89
+ const scenes: T[] = [sorted[0]!.spec.from];
90
+ const transitions: PlannedTransition[] = [];
91
+ for (const [i, item] of sorted.entries()) {
92
+ const prev = sorted[i - 1];
93
+ if (prev) {
94
+ if (prev.spec.to !== item.spec.from) {
95
+ throw new Error(`vfrey.transition: transitions run in a chain. The one at ${round(item.spec.at)} s leaves ${label(item.spec.from)}, but the one before it arrives at ${label(prev.spec.to)}.`);
96
+ }
97
+ if (item.spec.at < prev.spec.at + prev.duration - 1e-9) {
98
+ throw new Error(`vfrey.transition: the transition at ${round(item.spec.at)} s starts before the one at ${round(prev.spec.at)} s ends (${round(prev.spec.at + prev.duration)} s). Start it later, or shorten the first.`);
99
+ }
100
+ }
101
+ if (scenes.includes(item.spec.to)) throw new Error(`vfrey.transition: ${label(item.spec.to)} is shown twice in the chain; each shot appears once.`);
102
+ scenes.push(item.spec.to);
103
+ transitions.push({ time: round(item.spec.at), shader: item.type === "crossfade" ? undefined : item.type, duration: round(item.duration), ease: item.spec.ease ?? DEFAULT_TRANSITION_EASE });
104
+ }
105
+ return { scenes, transitions };
106
+ }
107
+
108
+ export interface TransitionState {
109
+ /** Indexes into `plan.scenes` that are on screen at `t`: one, or two inside a transition. */
110
+ visible: number[];
111
+ /** The transition running at `t`, or -1. */
112
+ index: number;
113
+ /** 0 to 1 through that transition, before the ease. 0 when none is running. */
114
+ progress: number;
115
+ }
116
+
117
+ /**
118
+ * What is on screen at time `t`. Framey's runtime shows shot i+1 from `time`, and removes shot i at `time + duration`,
119
+ * so both are on screen in between. A pure function of `t`, so a seek gives the same answer every time.
120
+ */
121
+ export function stateAt(p: TransitionPlan<unknown>, t: number): TransitionState {
122
+ let current = 0;
123
+ let index = -1;
124
+ let progress = 0;
125
+ for (const [i, tr] of p.transitions.entries()) {
126
+ if (t >= tr.time) current = i + 1;
127
+ if (t >= tr.time && t < tr.time + tr.duration) {
128
+ index = i;
129
+ progress = (t - tr.time) / tr.duration;
130
+ }
131
+ }
132
+ return { visible: index === -1 ? [current] : [index, index + 1], index, progress };
133
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * SVG sanitizing for the pictures an agent supplies. The picture is only ever shown through an <img> data URI
3
+ * (which cannot run scripts or load anything), and it is cleaned here as well, so a bad file is never carried
4
+ * through the job payload or the composition.
5
+ */
6
+
7
+ const ALLOWED_ELEMENTS = new Set([
8
+ "svg", "g", "defs", "path", "circle", "ellipse", "rect", "line", "polyline", "polygon", "text", "tspan", "title", "desc",
9
+ "lineargradient", "radialgradient", "stop", "clippath", "mask", "pattern", "symbol", "use", "marker", "style",
10
+ "filter", "fegaussianblur", "feoffset", "feblend", "fecolormatrix", "femerge", "femergenode", "fecomposite", "feflood", "fedropshadow",
11
+ ]);
12
+
13
+ /** Elements that are dropped together with everything inside them. */
14
+ const DROPPED_WITH_CONTENT = new Set(["script", "foreignobject", "iframe", "object", "embed", "audio", "video", "canvas", "animate", "set", "animatetransform", "animatemotion", "image", "a", "switch"]);
15
+
16
+ export interface SvgResult {
17
+ ok: true;
18
+ svg: string;
19
+ /** What was removed, for a warning. */
20
+ removed: string[];
21
+ }
22
+
23
+ export type SvgCheck = SvgResult | { ok: false; message: string };
24
+
25
+ const TAG = /<!--[\s\S]*?-->|<!\[CDATA\[[\s\S]*?\]\]>|<[?!][^>]*>|<\/?[A-Za-z][^\s/>]*(?:\s+(?:[^>"']|"[^"]*"|'[^']*')*)?\/?>/g;
26
+ const ATTR = /([^\s=/"'<>]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'>]+)))?/g;
27
+
28
+ function safeValue(name: string, value: string): boolean {
29
+ const v = value.replace(/[\s\u0000-\u001f]+/g, "").toLowerCase();
30
+ if (v.includes("javascript:") || v.includes("vbscript:") || v.includes("data:text/html") || v.includes("expression(") || v.includes("@import")) return false;
31
+ // Any url() must point inside the file.
32
+ for (const match of value.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi)) {
33
+ if (!match[2]!.trim().startsWith("#")) return false;
34
+ }
35
+ if ((name === "href" || name === "xlink:href") && !value.trim().startsWith("#")) return false;
36
+ return true;
37
+ }
38
+
39
+ function safeStyle(css: string): boolean {
40
+ const lower = css.toLowerCase();
41
+ if (lower.includes("@import") || lower.includes("expression(") || lower.includes("javascript:") || lower.includes("behavior")) return false;
42
+ for (const match of css.matchAll(/url\(\s*(['"]?)([^'")]*)\1\s*\)/gi)) {
43
+ if (!match[2]!.trim().startsWith("#")) return false;
44
+ }
45
+ return true;
46
+ }
47
+
48
+ function cleanAttributes(raw: string, removed: string[], tag: string): string {
49
+ const out: string[] = [];
50
+ const seen = new Set<string>();
51
+ for (const match of raw.matchAll(ATTR)) {
52
+ const name = match[1]!;
53
+ const lower = name.toLowerCase();
54
+ const value = match[2] ?? match[3] ?? match[4] ?? "";
55
+ if (seen.has(lower)) continue;
56
+ seen.add(lower);
57
+ if (lower.startsWith("on")) {
58
+ removed.push(`${tag}@${name}`);
59
+ continue;
60
+ }
61
+ if (lower === "style" && !safeStyle(value)) {
62
+ removed.push(`${tag}@style`);
63
+ continue;
64
+ }
65
+ if (lower !== "style" && !safeValue(lower, value)) {
66
+ removed.push(`${tag}@${name}`);
67
+ continue;
68
+ }
69
+ out.push(`${name}="${value.replace(/"/g, "&quot;")}"`);
70
+ }
71
+ return out.length ? " " + out.join(" ") : "";
72
+ }
73
+
74
+ /** Returns the cleaned SVG text. It fails only when the file is not an SVG at all. */
75
+ export function sanitizeSvg(input: string): SvgCheck {
76
+ const text = input.replace(/^/, "");
77
+ if (/<!ENTITY/i.test(text)) return { ok: false, message: "the SVG declares an XML entity, which is not allowed" };
78
+ const removed: string[] = [];
79
+ const out: string[] = [];
80
+ const stack: string[] = [];
81
+ let skipDepth = 0;
82
+ let skipName = "";
83
+ let last = 0;
84
+ let sawRoot = false;
85
+ let rootClosed = false;
86
+ let inStyle = false;
87
+ const emitText = (chunk: string) => {
88
+ if (skipDepth > 0 || !chunk) return;
89
+ if (inStyle) {
90
+ out.push(safeStyle(chunk) ? chunk : "");
91
+ if (!safeStyle(chunk)) removed.push("style");
92
+ return;
93
+ }
94
+ if (stack.length > 0) out.push(chunk.replace(/<(?![/A-Za-z])/g, "&lt;"));
95
+ };
96
+ for (const match of text.matchAll(TAG)) {
97
+ emitText(text.slice(last, match.index));
98
+ last = match.index! + match[0].length;
99
+ const token = match[0];
100
+ if (token.startsWith("<!--") || token.startsWith("<?") || token.startsWith("<!")) {
101
+ if (token.startsWith("<![CDATA[")) emitText(token);
102
+ continue;
103
+ }
104
+ const closing = token.startsWith("</");
105
+ const selfClosing = token.endsWith("/>");
106
+ const nameMatch = token.match(/^<\/?([^\s/>]+)/);
107
+ const rawName = nameMatch![1]!;
108
+ const name = rawName.replace(/^svg:/i, "");
109
+ const lower = name.toLowerCase();
110
+ if (skipDepth > 0) {
111
+ if (lower === skipName) {
112
+ if (closing) skipDepth -= 1;
113
+ else if (!selfClosing) skipDepth += 1;
114
+ }
115
+ continue;
116
+ }
117
+ if (closing) {
118
+ if (stack.length === 0) continue;
119
+ const top = stack.pop()!;
120
+ if (top.toLowerCase() === "style") inStyle = false;
121
+ out.push(`</${top}>`);
122
+ if (stack.length === 0) rootClosed = true;
123
+ continue;
124
+ }
125
+ if (DROPPED_WITH_CONTENT.has(lower)) {
126
+ removed.push(lower);
127
+ if (!selfClosing) {
128
+ skipDepth = 1;
129
+ skipName = lower;
130
+ }
131
+ continue;
132
+ }
133
+ if (!ALLOWED_ELEMENTS.has(lower)) {
134
+ removed.push(lower);
135
+ if (!selfClosing) {
136
+ skipDepth = 1;
137
+ skipName = lower;
138
+ }
139
+ continue;
140
+ }
141
+ if (rootClosed) continue;
142
+ if (!sawRoot) {
143
+ if (lower !== "svg") return { ok: false, message: "the file does not start with an <svg> element" };
144
+ sawRoot = true;
145
+ }
146
+ const attrs = cleanAttributes(token.slice(1 + rawName.length).replace(/\/?>$/, ""), removed, lower);
147
+ let finalAttrs = attrs;
148
+ if (lower === "svg" && stack.length === 0 && !/\sxmlns=/.test(finalAttrs)) finalAttrs += ' xmlns="http://www.w3.org/2000/svg"';
149
+ if (selfClosing) {
150
+ out.push(`<${name}${finalAttrs}/>`);
151
+ if (stack.length === 0) rootClosed = true;
152
+ } else {
153
+ out.push(`<${name}${finalAttrs}>`);
154
+ stack.push(name);
155
+ if (lower === "style") inStyle = true;
156
+ }
157
+ }
158
+ if (!sawRoot) return { ok: false, message: "the file has no <svg> element" };
159
+ while (stack.length) out.push(`</${stack.pop()!}>`);
160
+ return { ok: true, svg: out.join(""), removed };
161
+ }
162
+
163
+ /** The SVG's width and height ratio from its viewBox, or from width and height; null when it has neither. */
164
+ export function svgBox(svg: string): { width: number; height: number } | null {
165
+ const root = svg.match(/<svg\b[^>]*>/i)?.[0] ?? "";
166
+ const vb = root.match(/viewBox="\s*[-\d.eE]+[ ,]+[-\d.eE]+[ ,]+([\d.eE]+)[ ,]+([\d.eE]+)\s*"/i);
167
+ if (vb && Number(vb[1]) > 0 && Number(vb[2]) > 0) return { width: Number(vb[1]), height: Number(vb[2]) };
168
+ const w = root.match(/\swidth="([\d.]+)(?:px)?"/i);
169
+ const h = root.match(/\sheight="([\d.]+)(?:px)?"/i);
170
+ if (w && h && Number(w[1]) > 0 && Number(h[1]) > 0) return { width: Number(w[1]), height: Number(h[1]) };
171
+ return null;
172
+ }
173
+
174
+ /** Gives the root a viewBox when only width and height were set, so it scales inside any box. */
175
+ export function withViewBox(svg: string): string {
176
+ const root = svg.match(/<svg\b[^>]*>/i)?.[0] ?? "";
177
+ if (/viewBox=/i.test(root)) return svg;
178
+ const box = svgBox(svg);
179
+ if (!box) return svg;
180
+ return svg.replace(/<svg\b/i, `<svg viewBox="0 0 ${box.width} ${box.height}"`);
181
+ }
@@ -0,0 +1,152 @@
1
+ import { copyFile, lstat, mkdir, readdir, readFile, rm, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { sanitizeSvg } from "./sanitize.js";
4
+ import { REMOTION_TRANSITIONS_FIX } from "./transitions.js";
5
+
6
+ /** The most a scene folder may hold, in bytes. */
7
+ export const SCENE_MAX_BYTES = 50 * 1024 * 1024;
8
+
9
+ export type SceneKind = "html" | "tsx";
10
+
11
+ /** The engines that render each kind of scene, in the order the Fix line offers them. */
12
+ export const KIND_ENGINES: Record<SceneKind, string[]> = {
13
+ html: ["vfrey-framey", "hyperframes"],
14
+ tsx: ["remotion"],
15
+ };
16
+
17
+ export const KIND_ENTRY: Record<SceneKind, string> = { html: "index.html", tsx: "index.tsx" };
18
+
19
+ /** A scene that cannot be rendered as given. `message` says what is wrong and `fix` how to fix it. */
20
+ export class SceneError extends Error {
21
+ constructor(
22
+ message: string,
23
+ readonly fix: string,
24
+ readonly code: string = "scene_invalid",
25
+ ) {
26
+ super(message);
27
+ this.name = "SceneError";
28
+ }
29
+ }
30
+
31
+ export function kindForEngine(engineId: string): SceneKind | null {
32
+ if (engineId === "remotion") return "tsx";
33
+ if (engineId === "vfrey-framey" || engineId === "hyperframes") return "html";
34
+ return null;
35
+ }
36
+
37
+ async function exists(file: string): Promise<boolean> {
38
+ return stat(file).then(
39
+ () => true,
40
+ () => false,
41
+ );
42
+ }
43
+
44
+ /** Every file under `dir`, relative, skipping vfrey's own folder (it is rewritten at render time), node_modules, and the studio's `.hyperframes` history. */
45
+ async function listFiles(dir: string, base = dir): Promise<Array<{ rel: string; abs: string; link: boolean; size: number }>> {
46
+ const out: Array<{ rel: string; abs: string; link: boolean; size: number }> = [];
47
+ for (const entry of await readdir(dir, { withFileTypes: true })) {
48
+ const abs = path.join(dir, entry.name);
49
+ const rel = path.relative(base, abs).split(path.sep).join("/");
50
+ if (entry.name === ".DS_Store" || entry.name === "node_modules" || entry.name === ".git" || entry.name === ".hyperframes") continue;
51
+ if (dir === base && entry.name === "vfrey") continue;
52
+ if (entry.isSymbolicLink()) {
53
+ out.push({ rel, abs, link: true, size: 0 });
54
+ } else if (entry.isDirectory()) {
55
+ out.push(...(await listFiles(abs, base)));
56
+ } else if (entry.isFile()) {
57
+ out.push({ rel, abs, link: false, size: (await lstat(abs)).size });
58
+ }
59
+ }
60
+ return out;
61
+ }
62
+
63
+ /** Checks that the folder is a scene this engine can render. Throws a SceneError with the Fix line otherwise. */
64
+ export async function checkScene(dir: string, engineId: string): Promise<{ kind: SceneKind; bytes: number; files: number }> {
65
+ const kind = kindForEngine(engineId);
66
+ if (!kind) {
67
+ throw new SceneError(`A scene needs a render engine, and ${engineId} does not render scenes.`, "choose --engine vfrey-framey or --engine remotion.", "scene_engine_mismatch");
68
+ }
69
+ const info = await stat(dir).catch(() => null);
70
+ if (!info?.isDirectory()) {
71
+ throw new SceneError(
72
+ `The scene folder ${dir} does not exist.`,
73
+ `write ${path.join(dir, KIND_ENTRY[kind])} yourself, or run \`npx vfrey scene init ${dir}${kind === "tsx" ? " --engine remotion" : ""}\` for a starting point.`,
74
+ "scene_missing",
75
+ );
76
+ }
77
+ const hasHtml = await exists(path.join(dir, "index.html"));
78
+ const hasTsx = await exists(path.join(dir, "index.tsx"));
79
+ if (!(kind === "html" ? hasHtml : hasTsx)) {
80
+ const other: SceneKind = kind === "html" ? "tsx" : "html";
81
+ const found = other === "html" ? hasHtml : hasTsx;
82
+ const engine = engineName(engineId);
83
+ if (found) {
84
+ const fits = KIND_ENGINES[other];
85
+ throw new SceneError(
86
+ `The scene in ${dir} is ${other === "html" ? "an HTML composition (index.html)" : "a React component (index.tsx)"}, and ${engine} renders ${kind === "html" ? "HTML (index.html)" : "React (index.tsx)"}.`,
87
+ `render it with --engine ${fits[0]}${fits[1] ? ` (or ${fits[1]})` : ""}, or write ${KIND_ENTRY[kind]} for ${engine}.`,
88
+ "scene_engine_mismatch",
89
+ );
90
+ }
91
+ throw new SceneError(
92
+ `The scene folder ${dir} has no ${KIND_ENTRY[kind]}, which ${engine} needs.`,
93
+ `write ${path.join(dir, KIND_ENTRY[kind])}, or run \`npx vfrey scene init ${dir}${kind === "tsx" ? " --engine remotion" : ""}\` for a starting point.`,
94
+ "scene_missing",
95
+ );
96
+ }
97
+ if (kind === "tsx") {
98
+ const source = await readFile(path.join(dir, "index.tsx"), "utf8").catch(() => "");
99
+ if (/\bvfrey\.transition\s*\(|vfrey\/transitions\.js|\bHyperShader\b/.test(source)) {
100
+ throw new SceneError("Shader transitions are not supported in Remotion scenes; they are HTML-scene features (vfrey.transition with vfrey-framey or hyperframes).", REMOTION_TRANSITIONS_FIX, "scene_transitions_unsupported");
101
+ }
102
+ }
103
+ const files = await listFiles(dir);
104
+ const bytes = files.reduce((sum, f) => sum + f.size, 0);
105
+ if (bytes > SCENE_MAX_BYTES) {
106
+ throw new SceneError(
107
+ `The scene folder is ${(bytes / 1024 / 1024).toFixed(1)} MB; the limit is ${SCENE_MAX_BYTES / 1024 / 1024} MB.`,
108
+ "remove large images or videos from the scene folder; draw with SVG and CSS instead.",
109
+ "scene_too_large",
110
+ );
111
+ }
112
+ return { kind, bytes, files: files.length };
113
+ }
114
+
115
+ function engineName(id: string): string {
116
+ return id === "remotion" ? "Remotion" : id === "hyperframes" ? "HyperFrames" : "vfrey Framey";
117
+ }
118
+
119
+ /**
120
+ * Copies a scene folder to `dest`, the way vfrey keeps it with the run. Every SVG is sanitized, links that leave
121
+ * the folder are not followed, and vfrey's own `vfrey/` folder is left out (it is written fresh at render time).
122
+ * Returns what was dropped, as warnings for the run.
123
+ */
124
+ export async function storeScene(src: string, dest: string): Promise<{ warnings: string[]; bytes: number; files: number }> {
125
+ const warnings: string[] = [];
126
+ await rm(dest, { recursive: true, force: true });
127
+ await mkdir(dest, { recursive: true });
128
+ let bytes = 0;
129
+ let count = 0;
130
+ for (const file of await listFiles(src)) {
131
+ const target = path.join(dest, file.rel);
132
+ if (file.link) {
133
+ warnings.push(`${file.rel} is a symbolic link; it was not copied into the scene.`);
134
+ continue;
135
+ }
136
+ await mkdir(path.dirname(target), { recursive: true });
137
+ if (file.rel.toLowerCase().endsWith(".svg")) {
138
+ const checked = sanitizeSvg(await readFile(file.abs, "utf8"));
139
+ if (!checked.ok) {
140
+ warnings.push(`${file.rel} was left out: ${checked.message}.`);
141
+ continue;
142
+ }
143
+ if (checked.removed.length > 0) warnings.push(`${file.rel}: removed ${[...new Set(checked.removed)].join(", ")} while sanitizing.`);
144
+ await writeFile(target, checked.svg);
145
+ } else {
146
+ await copyFile(file.abs, target);
147
+ }
148
+ bytes += file.size;
149
+ count += 1;
150
+ }
151
+ return { warnings, bytes, files: count };
152
+ }
@@ -0,0 +1,48 @@
1
+ import path from "node:path";
2
+ import { browserDisabled, openBrowser } from "../dashboard-start.js";
3
+ import { SceneError } from "./scene.js";
4
+ import { parseStudioArgs, startStudio, type StudioHandle } from "./studio.js";
5
+
6
+ interface Io {
7
+ env: NodeJS.ProcessEnv;
8
+ stdout: (text: string) => void;
9
+ stderr: (text: string) => void;
10
+ /** Test hooks. The CLI omits them. */
11
+ open?: (url: string) => void;
12
+ /** Resolves when the studio should stop. The CLI waits for Ctrl+C or SIGTERM. */
13
+ stopped?: Promise<void>;
14
+ }
15
+
16
+ function untilSignal(): Promise<void> {
17
+ return new Promise((resolve) => {
18
+ const done = () => resolve();
19
+ process.once("SIGINT", done);
20
+ process.once("SIGTERM", done);
21
+ });
22
+ }
23
+
24
+ /** `vfrey scene studio DIR [--port N] [--no-browser]`: edit an HTML scene in Framey's studio until Ctrl+C. */
25
+ export async function runSceneStudio(argv: string[], io: Io): Promise<number> {
26
+ const parsed = parseStudioArgs(argv);
27
+ if (!parsed.ok) {
28
+ io.stderr(`✗ ${parsed.message}\nFix: ${parsed.fix}\n`);
29
+ return 1;
30
+ }
31
+ const { dir, port, noBrowser } = parsed.args;
32
+ let studio: StudioHandle;
33
+ try {
34
+ studio = await startStudio({ dir, port, env: io.env });
35
+ } catch (error) {
36
+ if (error instanceof SceneError) {
37
+ io.stderr(`✗ ${error.message}\nFix: ${error.fix}\n`);
38
+ return 1;
39
+ }
40
+ throw error;
41
+ }
42
+ io.stdout(`vfrey studio ${studio.url}\n`);
43
+ io.stderr(`Editing ${studio.dir}. Saves go to that folder. Render it as usual: npx vfrey render --scene ${path.relative(process.cwd(), studio.dir) || "."} --engine vfrey-framey\nPress Ctrl+C to stop the studio.\n`);
44
+ if (!noBrowser && !browserDisabled([], io.env)) (io.open ?? openBrowser)(studio.url);
45
+ await (io.stopped ?? untilSignal());
46
+ await studio.close();
47
+ return 0;
48
+ }