@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,728 @@
1
+ import path from "node:path";
2
+ import { readableVideoPath } from "../project/names.js";
3
+ import { storedVideoPath } from "../project/videos.js";
4
+ import { mkdir, writeFile } from "node:fs/promises";
5
+ import { randomUUID } from "node:crypto";
6
+ import { ComplianceError, ConfigError, QualityError, asAppError, backoffMs } from "@vfrey/contracts";
7
+ import { assertQualityVariation, selectVariation } from "@vfrey/contracts";
8
+ import type { Account, Candidate, ContentPackage, PipelineData, Platform, VariationSnapshot } from "@vfrey/contracts";
9
+ import { writePackage } from "../generate/index.js";
10
+ import { packageFromScript, type ScriptFile } from "../generate/script.js";
11
+ import { localDayKey } from "@vfrey/contracts";
12
+ import { log } from "../log.js";
13
+ import { accessToken } from "../oauth/tokens.js";
14
+ import { TokenVault } from "../oauth/vault.js";
15
+ import {
16
+ MAX_FREE_DEFERRALS,
17
+ POLL_WAIT_MS,
18
+ deferralOf,
19
+ proofOf,
20
+ publishFacebook,
21
+ publishInstagram,
22
+ publishTikTok,
23
+ publishYouTube,
24
+ type PublishContext,
25
+ type StoredSession,
26
+ } from "../publish/index.js";
27
+ import { LeaseLost, type Store, type JobRecord } from "../queue/store.js";
28
+ import { assertScore, interpretScore } from "@vfrey/ingest";
29
+ import { collectPerSource } from "./source-fetch.js";
30
+ import { synthesize } from "../tts.js";
31
+ import { engageAccount, type OwnedPost } from "../engage/index.js";
32
+ import { captureAnalytics, writeDashboard } from "../analytics/index.js";
33
+ import { probeVideo, type RenderedVideo } from "../render/probe.js";
34
+ import type { Runtime, SceneRecord, SceneRenderContext } from "./run.js";
35
+ import { readProjectEngine, chooseEngine, failedMessage, unavailableMessage } from "../product/engine-choice.js";
36
+ import { engineStatus, type EngineProbes } from "../product/engines.js";
37
+ import { defaultProbes } from "../product/probes.js";
38
+ import { canonicalPath } from "../project/canonical.js";
39
+ import { deskAccounts } from "../desk/series.js";
40
+ import { checkScene, SceneError, storeScene } from "../scene/scene.js";
41
+ import { topicCandidates, type UsedTopic } from "./topics-source.js";
42
+ import { wordTotal, failureMessage, gateOutcome, mergeChecks, realTools, reviewPackage, reviewRendered, type CheckResult } from "./review.js";
43
+
44
+ const PLATFORMS: Platform[] = ["youtube", "tiktok", "instagram", "facebook"];
45
+
46
+ /** True when a platform session holds a real upload: a stored upload address, container, log or id. A dry-run id is not one. */
47
+ export function hasRealSession(session: StoredSession | undefined): boolean {
48
+ if (!session) return false;
49
+ const realId = Boolean(session.externalId) && session.externalId !== "dry-run";
50
+ return realId || Boolean(session.uploadUrl) || Boolean(session.creationId) || Boolean(session.logId) || (session.uploadedBytes ?? 0) > 0;
51
+ }
52
+
53
+ /** Thrown at a safe point when the worker was asked to stop. The caller releases the lease. */
54
+ export class StopRequested extends Error {
55
+ constructor() {
56
+ super("stop requested");
57
+ this.name = "StopRequested";
58
+ }
59
+ }
60
+
61
+ /** The publish stage did not finish: some platform is paused or still processing. */
62
+ export interface PublishDeferral {
63
+ until: Date;
64
+ /** True when a platform failed in a way that should spend one of the job's attempts. */
65
+ countsAttempt: boolean;
66
+ code: string;
67
+ message: string;
68
+ }
69
+
70
+ export async function stageSource(runtime: Runtime, account: Account, data: PipelineData, job?: Pick<JobRecord, "id" | "attempt">): Promise<void> {
71
+ const manual = data.candidates.filter((candidate) => candidate.provider === "manual");
72
+ let fetched: Candidate[];
73
+ let topicsByFingerprint = new Map<string, UsedTopic>();
74
+ if (manual.length > 0) fetched = manual;
75
+ else {
76
+ // vfrey Topics come first: ranked, with their evidence. A failure skips only that source.
77
+ const topics = await topicCandidates(runtime, account, job);
78
+ const others = await collectPerSource(runtime, account).catch((error: unknown) => {
79
+ if (topics.length === 0) throw error;
80
+ log("warn", "other sources failed", { accountId: account.id, message: error instanceof Error ? error.message : String(error) });
81
+ return [] as Candidate[];
82
+ });
83
+ fetched = [...topics.map((t) => t.candidate), ...others];
84
+ topicsByFingerprint = new Map(topics.map((t) => [t.candidate.fingerprint, t.topic]));
85
+ }
86
+ const fresh = fetched.filter((candidate) => !runtime.store.seen(account.id, candidate.fingerprint));
87
+ if (fresh.length === 0) throw new QualityError("no_source", "No unseen candidates. This is a quality stop, not a compliance block.");
88
+ let chosen: Candidate | null = null;
89
+ for (const candidate of fresh) {
90
+ const raw = await runtime.llm.complete({
91
+ system: "Score a topic for an original short. Return JSON only.",
92
+ user: [
93
+ "TASK:SCORE",
94
+ `NICHE: ${account.niche}`,
95
+ `TITLE: ${candidate.title}`,
96
+ `EXCERPT: ${candidate.excerpt}`,
97
+ "Keys: score, safety, reason, riskClass, complianceTags.",
98
+ "complianceTags may include medical_advice_persona, legal_advice_persona, financial_advice_persona, impersonation, scam, copyrighted_transcript.",
99
+ ].join("\n"),
100
+ temperature: 0,
101
+ maxTokens: 400,
102
+ });
103
+ const score = interpretScore(JSON.parse(stripFence(raw)));
104
+ if (score.riskClass === "compliance") {
105
+ data.rejectedFingerprints.push(candidate.fingerprint);
106
+ runtime.store.markSeen(account.id, candidate.fingerprint, runtime.now());
107
+ log("info", "source compliance reject", { accountId: account.id, fingerprint: candidate.fingerprint, tags: score.complianceTags });
108
+ continue;
109
+ }
110
+ if (score.riskClass === "quality" || score.score < 70) {
111
+ data.rejectedFingerprints.push(candidate.fingerprint);
112
+ runtime.store.markSeen(account.id, candidate.fingerprint, runtime.now());
113
+ continue;
114
+ }
115
+ try {
116
+ assertScore(score);
117
+ } catch (error) {
118
+ if (error instanceof ComplianceError) {
119
+ data.rejectedFingerprints.push(candidate.fingerprint);
120
+ continue;
121
+ }
122
+ throw error;
123
+ }
124
+ chosen = candidate;
125
+ break;
126
+ }
127
+ if (!chosen) throw new QualityError("no_source", "Every candidate failed the score gate.");
128
+ data.candidates = [chosen];
129
+ const usedTopic = topicsByFingerprint.get(chosen.fingerprint);
130
+ // The agent and the script cite this: the topic, its evidence, and which topic id was used.
131
+ if (usedTopic) (data as PipelineData & { topic?: UsedTopic }).topic = usedTopic;
132
+ runtime.store.markSeen(account.id, chosen.fingerprint, runtime.now());
133
+ }
134
+
135
+ export async function stageGenerate(runtime: Runtime, account: Account, data: PipelineData): Promise<void> {
136
+ const candidate = data.candidates[0];
137
+ if (!candidate) throw new QualityError("no_source", "Generate ran without a candidate.");
138
+ const history = loadHistory(runtime.store, account.id, null);
139
+ const plan = selectVariation({
140
+ history,
141
+ voices: account.voices,
142
+ waiveAudioRotation: account.waiveAudioRotation,
143
+ });
144
+ // A script the agent wrote replaces the writer; every check below still runs on it.
145
+ const script = (data as PipelineData & { script?: ScriptFile }).script;
146
+ const drafted = script
147
+ ? packageFromScript({ script, candidate, plan, hashtagPool: account.hashtagPool })
148
+ : await writePackage({
149
+ llm: runtime.llm,
150
+ candidate,
151
+ plan,
152
+ niche: account.niche,
153
+ language: account.language,
154
+ hashtagPool: account.hashtagPool,
155
+ });
156
+ assertQualityVariation({
157
+ plan: drafted.plan,
158
+ hook: drafted.hook,
159
+ title: drafted.title,
160
+ hashtags: drafted.hashtags,
161
+ previous: history[0] ?? null,
162
+ voicePoolSize: account.voices.length,
163
+ waiveAudioRotation: account.waiveAudioRotation,
164
+ durationSec: drafted.durationSec,
165
+ });
166
+ data.content = { ...drafted, voiceoverPath: null, outputPath: null };
167
+ }
168
+
169
+ /**
170
+ * Renders the video. With the series' review switch on, the first pass stops after the script, scene and pre-render
171
+ * checks and returns `awaiting_review` (the caller parks the job); after Approve the same stage runs again and renders.
172
+ */
173
+ export async function stageRender(runtime: Runtime, account: Account, job: JobRecord, data: PipelineData): Promise<"rendered" | "awaiting_review"> {
174
+ const content = data.content;
175
+ if (!content) throw new QualityError("no_package", "Render ran without a package.");
176
+ const history = loadHistory(runtime.store, account.id, job.id);
177
+ assertQualityVariation({
178
+ plan: content.plan,
179
+ hook: content.hook,
180
+ title: content.title,
181
+ hashtags: content.hashtags,
182
+ previous: history[0] ?? null,
183
+ voicePoolSize: account.voices.length,
184
+ waiveAudioRotation: account.waiveAudioRotation,
185
+ durationSec: content.durationSec,
186
+ });
187
+ const rendering = !runtime.skipRender && !runtime.dryRun;
188
+ const pending = (data as PipelineData & { scene?: SceneRecord }).scene;
189
+ // A scene the agent wrote: this run's flag, or the copy an earlier attempt of this job already stored.
190
+ const sceneSource = rendering ? (runtime.scene ?? (pending?.source === "agent" ? pending.dir : null)) : null;
191
+ const choice = rendering
192
+ ? chooseEngine({ command: runtime.engine, series: runtime.store.seriesEngine(account.id), project: readProjectEngine(), machine: runtime.store.engineDefault() })
193
+ : null;
194
+ // The quality gate, before anything is rendered. A compliance failure blocks; a quality failure rejects.
195
+ const before = await reviewPackage({ content, account, scene: sceneSource && choice ? { dir: sceneSource, engineId: choice.engine.id } : null });
196
+ recordChecks(runtime, job, data, before);
197
+ assertGate(before);
198
+ let sceneKind: SceneRecord["kind"] = null;
199
+ if (sceneSource && choice) {
200
+ try {
201
+ sceneKind = (await checkScene(sceneSource, choice.engine.id)).kind;
202
+ } catch (error) {
203
+ if (error instanceof SceneError) throw new ConfigError(error.code, `${error.message} Fix: ${error.fix}`);
204
+ throw error;
205
+ }
206
+ }
207
+ const holder = data as PipelineData & { review?: { state?: string } };
208
+ if (runtime.store.seriesGates(account.id).reviewEnabled && holder.review?.state !== "approved") {
209
+ // Keep the scene with the job so the person can look at it, and so the render after Approve uses this exact copy.
210
+ if (sceneSource && choice) {
211
+ const dest = path.join(runtime.config.dataDir, "scenes", job.id);
212
+ const stored = canonicalPath(sceneSource) === canonicalPath(dest) ? { warnings: [], bytes: pending?.bytes ?? 0, files: pending?.files ?? 0 } : await storeScene(sceneSource, dest);
213
+ for (const message of stored.warnings) {
214
+ runtime.store.recordEvent(job.id, { stage: "render", status: "warning", code: "scene_warning", message, durationMs: null, attempt: job.attempt });
215
+ }
216
+ (data as PipelineData & { scene?: SceneRecord }).scene = { source: "agent", dir: dest, kind: sceneKind, bytes: stored.bytes, files: stored.files };
217
+ }
218
+ holder.review = { state: "waiting" };
219
+ data.content = content;
220
+ return "awaiting_review";
221
+ }
222
+ const audioDir = path.join(runtime.config.dataDir, "audio");
223
+ const audioExt = String(runtime.config.tts.provider) === "openai" ? "mp3" : "wav";
224
+ const voicePath = path.join(audioDir, `${job.id}.${audioExt}`);
225
+ content.voiceoverPath = await synthesize({
226
+ provider: runtime.config.tts.provider,
227
+ baseUrl: runtime.config.tts.baseUrl,
228
+ apiKey: runtime.config.tts.apiKey,
229
+ model: runtime.config.tts.model,
230
+ voice: content.plan.voiceId,
231
+ text: content.voiceoverScript,
232
+ outputPath: voicePath,
233
+ fetchImpl: runtime.fetchImpl,
234
+ });
235
+ if (runtime.config.tts.provider !== "none" && !content.voiceoverPath) {
236
+ throw new QualityError("audio_voice", "TTS produced no file.");
237
+ }
238
+ // A real render in a project is `videos/<date> <title>.mp4`, stored relative to the project root so the folder can move.
239
+ // The machine desk, and any dry run or skipped render (a placeholder file), use `renders/<jobId>.mp4` in the data folder.
240
+ const finished = Boolean(runtime.project) && rendering;
241
+ const outputPath = finished
242
+ ? readableVideoPath(path.join(runtime.project!.root, "videos"), runtime.now(), content.title)
243
+ : path.join(runtime.config.dataDir, "renders", `${job.id}.mp4`);
244
+ content.outputPath = finished ? path.relative(runtime.project!.root, outputPath).split(path.sep).join("/") : outputPath;
245
+ await mkdir(path.dirname(outputPath), { recursive: true });
246
+ if (content.voiceoverScript.trim()) {
247
+ await writeFile(outputPath.replace(/\.mp4$/, ".txt"), `${content.voiceoverScript.trim()}\n`);
248
+ }
249
+ if (rendering && choice) {
250
+ const probe = async (): Promise<EngineProbes> => (runtime.probes ? runtime.probes() : defaultProbes(runtime.env));
251
+ // Remotion needs no probe. Any other engine is checked first, so a missing one fails before a render starts.
252
+ if (choice.engine.id !== "remotion") {
253
+ const probes = await probe();
254
+ if (engineStatus(choice.engine, probes).availability === "unavailable") {
255
+ throw new ConfigError("engine_unavailable", unavailableMessage(choice.engine, probes) + (choice.source === "project" ? " (This engine is the one in vfrey.json.)" : ""));
256
+ }
257
+ }
258
+ let sceneCtx: SceneRenderContext | undefined;
259
+ if (sceneSource) {
260
+ const dest = path.join(runtime.config.dataDir, "scenes", job.id);
261
+ const stored = canonicalPath(sceneSource) === canonicalPath(dest) ? { warnings: [], bytes: pending?.bytes ?? 0, files: pending?.files ?? 0 } : await storeScene(sceneSource, dest);
262
+ const warn = (message: string) =>
263
+ runtime.store.recordEvent(job.id, { stage: "render", status: "warning", code: "scene_warning", message, durationMs: null, attempt: job.attempt });
264
+ for (const message of stored.warnings) warn(message);
265
+ (data as PipelineData & { scene?: SceneRecord }).scene = { source: "agent", dir: dest, kind: sceneKind, bytes: stored.bytes, files: stored.files };
266
+ sceneCtx = { dir: dest, warn };
267
+ } else {
268
+ (data as PipelineData & { scene?: SceneRecord }).scene = { source: "default", dir: null, kind: null, bytes: 0, files: 0 };
269
+ }
270
+ const renderOnce = async () => {
271
+ try {
272
+ await runtime.render(content, outputPath, choice.engine.id, sceneCtx);
273
+ } catch (error) {
274
+ if (error instanceof StopRequested) throw error;
275
+ throw new ConfigError("engine_failed", failedMessage(choice.engine, error, await probe()));
276
+ }
277
+ (data as PipelineData & { engine?: { id: string; label: string; source?: string } }).engine = { id: choice.engine.id, label: choice.engine.label, ...(choice.source === "project" ? { source: "project" } : {}) };
278
+ // The length shown anywhere is the rendered file's, not the template's plan.
279
+ (data as PipelineData & { rendered?: RenderedVideo | null }).rendered = await probeVideo(outputPath, runtime.env);
280
+ };
281
+ await renderOnce();
282
+ // The quality gate, on the finished file. One re-render when only quality checks failed: a render can change that.
283
+ const tools = runtime.review ?? realTools(runtime.env);
284
+ const inspect = (attempt: number) =>
285
+ reviewRendered({ file: outputPath, voicePath: content.voiceoverPath, labelBurned: Boolean(sceneSource), words: wordTotal(content.voiceoverScript), tools, attempt });
286
+ let after = await inspect(1);
287
+ recordChecks(runtime, job, data, after);
288
+ const first = gateOutcome(after);
289
+ if (first.kind === "rejected" && first.retry) {
290
+ runtime.store.recordEvent(job.id, {
291
+ stage: "review",
292
+ status: "retrying",
293
+ code: failureMessage(first.failed).code,
294
+ message: "The finished video failed a quality check. Rendering it once more.",
295
+ durationMs: null,
296
+ attempt: job.attempt,
297
+ });
298
+ await renderOnce();
299
+ after = await inspect(2);
300
+ recordChecks(runtime, job, data, after);
301
+ }
302
+ assertGate(after);
303
+ log("info", "rendered", { jobId: job.id, engine: choice.engine.id, source: choice.source });
304
+ } else {
305
+ await mkdir(path.dirname(outputPath), { recursive: true });
306
+ await writeFile(outputPath, Buffer.from("dry-run-mp4"));
307
+ }
308
+ data.content = content;
309
+ return "rendered";
310
+ }
311
+
312
+ /** Keeps the latest result of each check in the payload and writes one event per failure, or one for a clean pass. */
313
+ function recordChecks(runtime: Runtime, job: JobRecord, data: PipelineData, results: CheckResult[]): void {
314
+ const holder = data as PipelineData & { checks?: CheckResult[] };
315
+ holder.checks = mergeChecks(holder.checks, results);
316
+ const failed = results.filter((item) => item.status === "fail");
317
+ for (const item of failed) {
318
+ runtime.store.recordEvent(job.id, {
319
+ stage: "review",
320
+ status: "failed",
321
+ code: item.id,
322
+ message: `${item.severity}: ${item.message}${item.fix ? ` Fix: ${item.fix}` : ""}`,
323
+ durationMs: null,
324
+ attempt: job.attempt,
325
+ });
326
+ }
327
+ if (failed.length === 0 && results.length > 0) {
328
+ const phase = results[0]!.phase === "before" ? "before the render" : "on the finished video";
329
+ runtime.store.recordEvent(job.id, {
330
+ stage: "review",
331
+ status: "passed",
332
+ code: null,
333
+ message: `${results.filter((item) => item.status === "pass").length} checks passed ${phase}.`,
334
+ durationMs: null,
335
+ attempt: job.attempt,
336
+ });
337
+ }
338
+ }
339
+
340
+ /** Compliance failure: blocked, no retry. Quality failure: rejected. Both carry the first Fix. */
341
+ function assertGate(results: CheckResult[]): void {
342
+ const outcome = gateOutcome(results);
343
+ if (outcome.kind === "pass") return;
344
+ const { code, message } = failureMessage(outcome.failed);
345
+ if (outcome.kind === "blocked") throw new ComplianceError(code, message);
346
+ throw new QualityError(code, message);
347
+ }
348
+
349
+ export async function stagePublish(
350
+ runtime: Runtime,
351
+ account: Account,
352
+ job: JobRecord,
353
+ data: PipelineData,
354
+ ): Promise<PublishDeferral | null> {
355
+ const content = data.content;
356
+ if (!content?.outputPath) throw new QualityError("no_render", "Publish ran without a file.");
357
+ // This job's own earlier rows are not "the previous video".
358
+ const history = loadHistory(runtime.store, account.id, job.id);
359
+ assertQualityVariation({
360
+ plan: content.plan,
361
+ hook: content.hook,
362
+ title: content.title,
363
+ hashtags: content.hashtags,
364
+ previous: history[0] ?? null,
365
+ voicePoolSize: account.voices.length,
366
+ waiveAudioRotation: account.waiveAudioRotation,
367
+ durationSec: content.durationSec,
368
+ });
369
+ const vault = new TokenVault(path.join(runtime.config.dataDir, "tokens.json"));
370
+ const nowMs = runtime.now().getTime();
371
+ // The posting cap is checked here, so a video made on request still never exceeds it. A job that
372
+ // already published somewhere is finishing its own video, not adding one.
373
+ const day = localDayKey(runtime.now(), account.cadence.timezone);
374
+ const postedToday = runtime.store
375
+ .publicationTimes(account.id, 200)
376
+ .filter((time) => localDayKey(time, account.cadence.timezone) === day).length;
377
+ const capped = !runtime.store.jobHasPublication(job.id) && postedToday >= account.cadence.maxPostsPerDay;
378
+ // A series that is not live runs the whole pipeline and records a dry-run publication, but never uploads.
379
+ const gates = runtime.store.seriesGates(account.id);
380
+ const dry = runtime.dryRun || !gates.live;
381
+ for (const platform of PLATFORMS) {
382
+ if (runtime.shouldStop?.()) throw new StopRequested();
383
+ // A cancelled or taken-over claim never starts an upload.
384
+ if (!runtime.store.holdsLease(job)) throw new LeaseLost(job.id);
385
+ const existing = data.platforms[platform] as StoredSession | undefined;
386
+ if (existing?.phase === "done" || existing?.phase === "skipped" || existing?.phase === "failed") {
387
+ if (existing.phase === "done" && existing.disclosure) recordPublication(runtime, account, job, content, platform, existing);
388
+ continue;
389
+ }
390
+ const enabled = account.platforms[platform]?.enabled;
391
+ if (!enabled) {
392
+ data.platforms[platform] = {
393
+ phase: "skipped",
394
+ externalId: null,
395
+ uploadUrl: null,
396
+ creationId: null,
397
+ logId: null,
398
+ disclosure: null,
399
+ errorCode: null,
400
+ errorMessage: null,
401
+ uploadedBytes: 0,
402
+ };
403
+ runtime.store.save(job);
404
+ continue;
405
+ }
406
+ // A publication row means this platform already has the video, whatever the stored session says.
407
+ if (runtime.store.publicationExists(job.id, platform)) {
408
+ data.platforms[platform] = { ...blankSession(), phase: "done", externalId: existing?.externalId ?? null };
409
+ runtime.store.save(job);
410
+ continue;
411
+ }
412
+ if (existing?.resumeAt && Date.parse(existing.resumeAt) > nowMs) continue;
413
+ // Publish off keeps the video and uploads nothing. A session that is not live any more is abandoned with its ids kept,
414
+ // never overwritten by a dry run and never started again.
415
+ if (!gates.publishEnabled || (dry && hasRealSession(existing))) {
416
+ const off = !gates.publishEnabled;
417
+ data.platforms[platform] = {
418
+ ...(existing ?? blankSession()),
419
+ phase: "skipped",
420
+ errorCode: off ? "publish_off" : "not_live",
421
+ errorMessage: off
422
+ ? "The series' publish switch is off. The video is kept and not uploaded."
423
+ : "The series is not live, so this unfinished upload was left alone. It will not be started again.",
424
+ };
425
+ delete (data.platforms[platform] as StoredSession).resumeAt;
426
+ runtime.store.save(job);
427
+ log("info", "publish skipped", { accountId: account.id, platform, reason: off ? "publish_off" : "not_live" });
428
+ continue;
429
+ }
430
+ if (capped) {
431
+ data.platforms[platform] = {
432
+ ...blankSession(),
433
+ phase: "skipped",
434
+ errorCode: "daily_cap",
435
+ errorMessage: `The series reached its posting cap of ${account.cadence.maxPostsPerDay} for today. The video is kept and not uploaded.`,
436
+ };
437
+ runtime.store.save(job);
438
+ log("info", "publish skipped", { accountId: account.id, platform, reason: "daily_cap" });
439
+ continue;
440
+ }
441
+ if (!dry && platformKeyMissing(account, platform, runtime.env)) {
442
+ const name = platform === "youtube" ? "YouTube" : platform;
443
+ data.platforms[platform] = {
444
+ ...blankSession(),
445
+ phase: "skipped",
446
+ errorCode: "missing_key",
447
+ errorMessage: `The ${name} key is not configured. This video cannot be pushed there.`,
448
+ };
449
+ runtime.store.save(job);
450
+ log("info", "publish skipped", { accountId: account.id, platform, reason: "missing_key" });
451
+ continue;
452
+ }
453
+ try {
454
+ const token = dry
455
+ ? "dry-run"
456
+ : await accessToken({
457
+ config: runtime.config,
458
+ account,
459
+ platform,
460
+ env: runtime.env,
461
+ vault,
462
+ fetchImpl: runtime.fetchImpl,
463
+ now: runtime.now(),
464
+ });
465
+ const ctx: PublishContext = {
466
+ config: runtime.config,
467
+ account,
468
+ content,
469
+ filePath: storedVideoPath(runtime.config.dataDir, content.outputPath),
470
+ fetchImpl: runtime.fetchImpl,
471
+ store: runtime.store,
472
+ now: runtime.now(),
473
+ env: runtime.env,
474
+ token,
475
+ dryRun: dry,
476
+ session: existing ?? blankSession(),
477
+ uploadIdleMs: runtime.uploadIdleMs,
478
+ checkpoint: (next) => {
479
+ data.platforms[platform] = next;
480
+ job.payload = data;
481
+ runtime.store.save(job);
482
+ },
483
+ };
484
+ const session: StoredSession =
485
+ platform === "youtube"
486
+ ? await publishYouTube(ctx)
487
+ : platform === "tiktok"
488
+ ? await publishTikTok(ctx)
489
+ : platform === "instagram"
490
+ ? await publishInstagram(ctx)
491
+ : await publishFacebook(ctx);
492
+ delete session.resumeAt;
493
+ delete session.resumeFree;
494
+ data.platforms[platform] = session;
495
+ data.dryRunPayloads[platform] = session.disclosure;
496
+ runtime.store.save(job);
497
+ if (session.phase === "done") recordPublication(runtime, account, job, content, platform, session);
498
+ } catch (error) {
499
+ if (error instanceof StopRequested || error instanceof LeaseLost) throw error;
500
+ const appError = asAppError(error);
501
+ const saved = (data.platforms[platform] ?? existing) as StoredSession | undefined;
502
+ const stored: StoredSession = {
503
+ phase: saved?.phase && saved.phase !== "idle" ? saved.phase : "idle",
504
+ externalId: saved?.externalId ?? null,
505
+ uploadUrl: saved?.uploadUrl ?? null,
506
+ creationId: saved?.creationId ?? null,
507
+ logId: saved?.logId ?? null,
508
+ disclosure: saved?.disclosure ?? null,
509
+ uploadedBytes: saved?.uploadedBytes ?? 0,
510
+ errorCode: appError.code,
511
+ errorMessage: appError.message,
512
+ errorRisk: appError.risk,
513
+ deferrals: saved?.deferrals ?? 0,
514
+ };
515
+ if (appError.retryable) {
516
+ const info = deferralOf(appError);
517
+ const free = info.free && stored.deferrals! < MAX_FREE_DEFERRALS;
518
+ if (free) stored.deferrals = stored.deferrals! + 1;
519
+ // Our own wait: a server pause is used as given, not cut to the backoff cap.
520
+ const wait = free
521
+ ? info.retryAfterMs ?? POLL_WAIT_MS
522
+ : backoffMs({ attempt: job.attempt, retryAfterMs: info.retryAfterMs, random: () => 0.5 });
523
+ stored.resumeAt = new Date(runtime.now().getTime() + wait).toISOString();
524
+ stored.resumeFree = free;
525
+ } else {
526
+ stored.phase = "failed";
527
+ }
528
+ data.platforms[platform] = stored;
529
+ runtime.store.save(job);
530
+ runtime.store.setHealth(account.id, platform, appError.retryable ? "retrying" : "blocked", appError.code, runtime.now());
531
+ if (!appError.retryable) {
532
+ log("error", "platform isolated", { accountId: account.id, platform, code: appError.code, risk: appError.risk });
533
+ }
534
+ }
535
+ }
536
+ const pending = PLATFORMS.filter((platform) => {
537
+ if (!account.platforms[platform]?.enabled) return false;
538
+ const phase = data.platforms[platform]?.phase;
539
+ return phase !== "done" && phase !== "skipped" && phase !== "failed";
540
+ });
541
+ if (pending.length > 0) {
542
+ const sessions = pending.map((platform) => data.platforms[platform] as StoredSession | undefined);
543
+ const times = sessions.map((item) => (item?.resumeAt ? Date.parse(item.resumeAt) : nowMs));
544
+ const first = sessions[times.indexOf(Math.min(...times))];
545
+ const summary = pending.map((platform) => `${platform}: ${data.platforms[platform]?.errorCode ?? "waiting"}`).join("; ");
546
+ return {
547
+ until: new Date(Math.min(...times)),
548
+ countsAttempt: sessions.some((item) => item?.resumeFree !== true),
549
+ code: first?.errorCode ?? "publish_pending",
550
+ message: summary,
551
+ };
552
+ }
553
+ const anyDone = PLATFORMS.some((platform) => data.platforms[platform]?.phase === "done");
554
+ if (!anyDone) {
555
+ const enabled = PLATFORMS.filter((platform) => account.platforms[platform]?.enabled);
556
+ if (enabled.length === 0) return null;
557
+ // A platform with no key never tried; it says nothing about the video.
558
+ const failed = enabled.filter((platform) => data.platforms[platform]?.phase === "failed");
559
+ if (failed.length === 0) return null;
560
+ const detail = "Every enabled platform failed. See per-platform error codes on the job.";
561
+ const risks = failed.map((platform) => data.platforms[platform]?.errorRisk);
562
+ // An unknown risk is treated as compliance: disclosure stays fail-closed.
563
+ if (risks.some((risk) => risk === "compliance" || !risk)) throw new ComplianceError("publish_all_failed", detail);
564
+ if (risks.some((risk) => risk === "config")) throw new ConfigError("publish_all_failed", detail);
565
+ throw new QualityError("publish_all_failed", detail);
566
+ }
567
+ return null;
568
+ }
569
+
570
+ function blankSession(): StoredSession {
571
+ return {
572
+ phase: "idle",
573
+ externalId: null,
574
+ uploadUrl: null,
575
+ creationId: null,
576
+ logId: null,
577
+ disclosure: null,
578
+ errorCode: null,
579
+ errorMessage: null,
580
+ uploadedBytes: 0,
581
+ };
582
+ }
583
+
584
+ function recordPublication(
585
+ runtime: Runtime,
586
+ account: Account,
587
+ job: JobRecord,
588
+ content: ContentPackage,
589
+ platform: Platform,
590
+ session: NonNullable<PipelineData["platforms"][Platform]>,
591
+ ): void {
592
+ if (!session.disclosure || runtime.store.publicationExists(job.id, platform)) return;
593
+ proofOf(session);
594
+ runtime.store.insertPublication({
595
+ id: randomUUID(),
596
+ jobId: job.id,
597
+ accountId: account.id,
598
+ platform,
599
+ externalId: session.externalId,
600
+ disclosure: session.disclosure,
601
+ variation: content.plan,
602
+ hook: content.hook,
603
+ title: content.title,
604
+ hashtags: content.hashtags,
605
+ script: content.voiceoverScript,
606
+ publishedAt: runtime.now().toISOString(),
607
+ audit: { logId: session.logId, uploadUrlPresent: Boolean(session.uploadUrl), ...(session.externalId === "dry-run" ? { dryRun: true } : {}) },
608
+ });
609
+ }
610
+
611
+ export async function runEngage(runtime: Runtime, account: Account, data: PipelineData | null): Promise<number> {
612
+ const posts = ownedPosts(runtime, account, data);
613
+ const vault = new TokenVault(path.join(runtime.config.dataDir, "tokens.json"));
614
+ return engageAccount({
615
+ config: runtime.config,
616
+ account,
617
+ posts,
618
+ llm: runtime.llm,
619
+ store: runtime.store,
620
+ fetchImpl: runtime.fetchImpl,
621
+ now: runtime.now(),
622
+ // Replies are public. A series that is not live drafts nothing and sends nothing.
623
+ dryRun: runtime.dryRun || !runtime.store.seriesGates(account.id).live,
624
+ tokenFor: (platform) =>
625
+ accessToken({
626
+ config: runtime.config,
627
+ account,
628
+ platform,
629
+ env: runtime.env,
630
+ vault,
631
+ fetchImpl: runtime.fetchImpl,
632
+ now: runtime.now(),
633
+ }),
634
+ });
635
+ }
636
+
637
+ /** True when no platform of this series has a key, so there is nobody to read comments from or reply to. */
638
+ export function noPlatformKeys(runtime: Runtime, account: Account): boolean {
639
+ if (runtime.dryRun) return false;
640
+ return PLATFORMS.every((platform) => platformKeyMissing(account, platform, runtime.env));
641
+ }
642
+
643
+ function platformKeyMissing(account: Account, platform: Platform, env: Runtime["env"]): boolean {
644
+ const row = account.platforms[platform] as { refreshTokenEnv?: string; accessTokenEnv?: string; pageTokenEnv?: string } | undefined;
645
+ if (!row) return true;
646
+ const envName =
647
+ platform === "youtube" || platform === "tiktok"
648
+ ? row.refreshTokenEnv
649
+ : platform === "instagram"
650
+ ? row.accessTokenEnv
651
+ : row.pageTokenEnv;
652
+ if (!envName) return true;
653
+ return !env[envName]?.trim();
654
+ }
655
+
656
+ function ownedPosts(runtime: Runtime, account: Account, data: PipelineData | null): OwnedPost[] {
657
+ const posts: OwnedPost[] = [];
658
+ const rows = runtime.store.recentPublications(account.id, 10);
659
+ for (const row of rows) {
660
+ const platform = String(row.platform);
661
+ const externalId = row.external_id ? String(row.external_id) : "";
662
+ if (!externalId || externalId === "dry-run") continue;
663
+ if (platform === "youtube" || platform === "instagram" || platform === "facebook") {
664
+ posts.push({ platform, externalId });
665
+ }
666
+ }
667
+ if (data) {
668
+ for (const platform of ["youtube", "instagram", "facebook"] as const) {
669
+ const externalId = data.platforms[platform]?.externalId;
670
+ if (externalId && externalId !== "dry-run" && !posts.some((post) => post.externalId === externalId)) {
671
+ posts.push({ platform, externalId });
672
+ }
673
+ }
674
+ }
675
+ return posts;
676
+ }
677
+
678
+ export async function runAnalytics(runtime: Runtime, account: Account): Promise<void> {
679
+ const posts = runtime.store.recentPublications(account.id, 20).map((row) => ({
680
+ platform: String(row.platform),
681
+ externalId: row.external_id ? String(row.external_id) : "",
682
+ }));
683
+ const vault = new TokenVault(path.join(runtime.config.dataDir, "tokens.json"));
684
+ if (!runtime.dryRun) {
685
+ await captureAnalytics({
686
+ config: runtime.config,
687
+ account,
688
+ posts,
689
+ store: runtime.store,
690
+ fetchImpl: runtime.fetchImpl,
691
+ now: runtime.now(),
692
+ tokenFor: (platform) =>
693
+ accessToken({
694
+ config: runtime.config,
695
+ account,
696
+ platform: platform as Platform,
697
+ env: runtime.env,
698
+ vault,
699
+ fetchImpl: runtime.fetchImpl,
700
+ now: runtime.now(),
701
+ }),
702
+ });
703
+ }
704
+ await writeDashboard({
705
+ store: runtime.store,
706
+ accounts: deskAccounts(runtime.store),
707
+ dataDir: runtime.config.dataDir,
708
+ now: runtime.now(),
709
+ });
710
+ }
711
+
712
+ function loadHistory(store: Store, accountId: string, excludeJobId: string | null): VariationSnapshot[] {
713
+ return store.recentVideos(accountId, 8, excludeJobId).map((row) => {
714
+ const plan = JSON.parse(String(row.variation)) as VariationSnapshot;
715
+ return {
716
+ ...plan,
717
+ hook: String(row.hook),
718
+ title: String(row.title),
719
+ hashtags: JSON.parse(String(row.hashtags)) as string[],
720
+ script: String(row.script),
721
+ };
722
+ });
723
+ }
724
+
725
+ function stripFence(text: string): string {
726
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
727
+ return (fenced?.[1] ?? text).trim();
728
+ }