@vfrey/vfrey 0.0.1 → 1.0.0-beta.1
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.
- package/LICENSE +33 -0
- package/README.md +78 -3
- package/accounts.example.json +61 -0
- package/accounts.first-run.json +41 -0
- package/bin/vfrey.mjs +36 -0
- package/integration-tools/README.md +43 -0
- package/integration-tools/image/pillow.md +32 -0
- package/integration-tools/image/playwright.md +33 -0
- package/integration-tools/image/resvg.md +36 -0
- package/integration-tools/image/satori.md +36 -0
- package/integration-tools/image/sharp.md +38 -0
- package/integration-tools/video/blender.md +34 -0
- package/integration-tools/video/ffmpeg.md +41 -0
- package/integration-tools/video/manim.md +37 -0
- package/integration-tools/video/motion-canvas.md +37 -0
- package/integration-tools/video/remotion.md +50 -0
- package/integration-tools/video/revideo.md +32 -0
- package/package.json +74 -6
- package/skills/vfrey-orchestrator/SKILL.md +15 -0
- package/skills/vfrey-orchestrator/channel-video/SKILL.md +41 -0
- package/skills/vfrey-product/SKILL.md +63 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/framey/index.html +70 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-one.svg +14 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/cook-two.svg +22 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/remotion/index.tsx +49 -0
- package/skills/vfrey-product/examples/concurrency-vs-parallelism/script.json +15 -0
- package/skills/vfrey-product/examples/octopus-facts/index.html +60 -0
- package/skills/vfrey-product/examples/octopus-facts/octopus.svg +8 -0
- package/skills/vfrey-product/examples/octopus-facts/script.json +14 -0
- package/skills/vfrey-product/push/SKILL.md +15 -0
- package/skills/vfrey-product/scenes/NOTICE +5 -0
- package/skills/vfrey-product/scenes/blocks.md +57 -0
- package/skills/vfrey-product/scenes/craft.md +118 -0
- package/skills/vfrey-product/scenes/motion.md +88 -0
- package/src/analytics/dashboard.ts +109 -0
- package/src/analytics/index.ts +260 -0
- package/src/api/client.ts +361 -0
- package/src/api/cloud-files.ts +197 -0
- package/src/api/cloud-runs.ts +75 -0
- package/src/api/cloud.ts +413 -0
- package/src/api/commands.ts +330 -0
- package/src/api/config.ts +34 -0
- package/src/api/creators.ts +463 -0
- package/src/api/credentials.ts +152 -0
- package/src/api/mail.ts +333 -0
- package/src/api/topics.ts +207 -0
- package/src/api/types.ts +273 -0
- package/src/cli.ts +468 -0
- package/src/cloud/bundle.ts +70 -0
- package/src/cloud/cloudrun-executor.ts +237 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/gcs-sign.ts +76 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/remote-job.ts +67 -0
- package/src/cloud/render-job.ts +191 -0
- package/src/config.ts +309 -0
- package/src/dashboard-link.ts +180 -0
- package/src/dashboard-start.ts +103 -0
- package/src/desk/channel.ts +64 -0
- package/src/desk/cli.ts +216 -0
- package/src/desk/creators-route.ts +129 -0
- package/src/desk/fonts/JetBrainsMono-OFL.txt +93 -0
- package/src/desk/fonts/google-sans-flex-latin.woff2 +0 -0
- package/src/desk/fonts/jetbrains-mono-medium.woff2 +0 -0
- package/src/desk/http.ts +460 -0
- package/src/desk/mail-route.ts +103 -0
- package/src/desk/overlay.ts +112 -0
- package/src/desk/preview.ts +422 -0
- package/src/desk/screens.ts +149 -0
- package/src/desk/secrets.ts +143 -0
- package/src/desk/series.ts +127 -0
- package/src/desk/state.ts +77 -0
- package/src/desk/studio-route.ts +119 -0
- package/src/desk/tick-runner.ts +66 -0
- package/src/desk/tick.ts +56 -0
- package/src/desk/topics-route.ts +41 -0
- package/src/desk/ui/app.css +505 -0
- package/src/desk/ui/app.js +185 -0
- package/src/desk/ui/apple-icon.png +0 -0
- package/src/desk/ui/components.js +79 -0
- package/src/desk/ui/core.js +156 -0
- package/src/desk/ui/favicon.ico +0 -0
- package/src/desk/ui/icon.svg +1 -0
- package/src/desk/ui/icons.js +49 -0
- package/src/desk/ui/index.html +41 -0
- package/src/desk/ui/metrics-math.d.ts +22 -0
- package/src/desk/ui/metrics-math.js +135 -0
- package/src/desk/ui/theme.js +5 -0
- package/src/desk/ui/views-creators.js +209 -0
- package/src/desk/ui/views-keys.js +114 -0
- package/src/desk/ui/views-mail.js +188 -0
- package/src/desk/ui/views-misc.js +123 -0
- package/src/desk/ui/views-review.js +119 -0
- package/src/desk/ui/views-runs.js +350 -0
- package/src/desk/ui/views-screens.js +163 -0
- package/src/desk/ui/views-series.js +318 -0
- package/src/desk/ui/views-topics.js +87 -0
- package/src/desk/verbs.ts +361 -0
- package/src/doctor.ts +212 -0
- package/src/drain.ts +14 -0
- package/src/engage/index.ts +217 -0
- package/src/generate/index.ts +110 -0
- package/src/generate/script.ts +152 -0
- package/src/generate/title.ts +78 -0
- package/src/help.ts +252 -0
- package/src/home.ts +45 -0
- package/src/llm/client.ts +177 -0
- package/src/log.ts +7 -0
- package/src/oauth/tokens.ts +189 -0
- package/src/oauth/vault.ts +72 -0
- package/src/pipeline/review.ts +642 -0
- package/src/pipeline/run.ts +443 -0
- package/src/pipeline/series-guard.ts +26 -0
- package/src/pipeline/source-fetch.ts +69 -0
- package/src/pipeline/stages.ts +728 -0
- package/src/pipeline/topics-source.ts +90 -0
- package/src/product/commands.ts +296 -0
- package/src/product/config-file.ts +38 -0
- package/src/product/engine-choice.ts +89 -0
- package/src/product/engines.ts +103 -0
- package/src/product/insights.ts +50 -0
- package/src/product/keys.ts +67 -0
- package/src/product/paths.ts +18 -0
- package/src/product/probes.ts +88 -0
- package/src/product/push-flow.ts +90 -0
- package/src/product/render-video.ts +21 -0
- package/src/product/short.ts +62 -0
- package/src/product/update.ts +32 -0
- package/src/project/canonical.ts +14 -0
- package/src/project/commands.ts +177 -0
- package/src/project/dashboard-status.ts +28 -0
- package/src/project/discover.ts +19 -0
- package/src/project/index-file.ts +73 -0
- package/src/project/marker.ts +92 -0
- package/src/project/migrate.ts +201 -0
- package/src/project/names.ts +27 -0
- package/src/project/notice.ts +16 -0
- package/src/project/open.ts +88 -0
- package/src/project/scaffold.ts +112 -0
- package/src/project/templates.ts +42 -0
- package/src/project/videos.ts +64 -0
- package/src/publish/facebook.ts +91 -0
- package/src/publish/index.ts +12 -0
- package/src/publish/instagram.ts +97 -0
- package/src/publish/rate.ts +62 -0
- package/src/publish/session.ts +242 -0
- package/src/publish/tiktok.ts +202 -0
- package/src/publish/youtube.ts +177 -0
- package/src/queue/desk-ops.ts +1170 -0
- package/src/queue/seal.ts +3 -0
- package/src/queue/store.ts +1006 -0
- package/src/remotion/AiLabel.tsx +26 -0
- package/src/remotion/Root.tsx +73 -0
- package/src/remotion/defaults.ts +26 -0
- package/src/remotion/films/claude/ClaudeFilm.tsx +1014 -0
- package/src/remotion/films/engine/EngineMark.tsx +17 -0
- package/src/remotion/films/rea/ReaShort.tsx +552 -0
- package/src/remotion/films/rea/drawings.tsx +907 -0
- package/src/remotion/films/rea/timing.ts +144 -0
- package/src/remotion/index.ts +4 -0
- package/src/remotion/templates/EditorialSplit.tsx +39 -0
- package/src/remotion/templates/KineticType.tsx +39 -0
- package/src/remotion/templates/SignalBoard.tsx +50 -0
- package/src/remotion/timing.ts +22 -0
- package/src/render/chrome.ts +78 -0
- package/src/render/engine-mark.ts +29 -0
- package/src/render/framey-template.ts +130 -0
- package/src/render/html-engine.ts +149 -0
- package/src/render/node-crypto-stub.ts +11 -0
- package/src/render/probe.ts +29 -0
- package/src/render/props.ts +17 -0
- package/src/render/vertical-short.ts +35 -0
- package/src/render/webpack.ts +33 -0
- package/src/render/worker.ts +65 -0
- package/src/scene/assets/fonts/TitanOne-OFL.txt +94 -0
- package/src/scene/assets/fonts/titan-one.woff2 +0 -0
- package/src/scene/assets/gsap/LICENSE.txt +8 -0
- package/src/scene/assets/gsap/gsap.min.js +11 -0
- package/src/scene/audio.ts +54 -0
- package/src/scene/blocks/LICENSE +190 -0
- package/src/scene/blocks/NOTICE +5 -0
- package/src/scene/blocks/bar-chart-race/bar-chart-race.html +585 -0
- package/src/scene/blocks/beat-freeze-cut/beat-freeze-cut.html +874 -0
- package/src/scene/blocks/camcorder-hud/camcorder-hud.html +164 -0
- package/src/scene/blocks/camera-dolly-zoom/camera-dolly-zoom.html +355 -0
- package/src/scene/blocks/code-diff/code-diff.html +817 -0
- package/src/scene/blocks/code-snippet-apple-terminal-pro/code-snippet-apple-terminal-pro.html +248 -0
- package/src/scene/blocks/code-typing/code-typing.html +764 -0
- package/src/scene/blocks/data-chart/data-chart.html +452 -0
- package/src/scene/blocks/flowchart-vertical/flowchart-vertical.html +454 -0
- package/src/scene/blocks/hw-scribble-transition/hw-scribble-transition.html +153 -0
- package/src/scene/blocks/index.json +824 -0
- package/src/scene/blocks/lt-accent-underline/lt-accent-underline.html +107 -0
- package/src/scene/blocks/lt-bold-block/lt-bold-block.html +109 -0
- package/src/scene/blocks/lt-clean-bar/lt-clean-bar.html +112 -0
- package/src/scene/blocks/lt-dark-card/lt-dark-card.html +111 -0
- package/src/scene/blocks/lt-kicker-name/lt-kicker-name.html +114 -0
- package/src/scene/blocks/lt-mask-reveal/lt-mask-reveal.html +120 -0
- package/src/scene/blocks/lt-neon-border/lt-neon-border.html +391 -0
- package/src/scene/blocks/lt-stack-bars/lt-stack-bars.html +109 -0
- package/src/scene/blocks/macos-notification/macos-notification.html +194 -0
- package/src/scene/blocks/mk-background/mk-background.html +236 -0
- package/src/scene/blocks/mk-callout-highlight/mk-callout-highlight.html +183 -0
- package/src/scene/blocks/mk-clone-wall-transition/mk-clone-wall-transition.html +226 -0
- package/src/scene/blocks/mk-line-graph/mk-line-graph.html +293 -0
- package/src/scene/blocks/mk-progress-stat/mk-progress-stat.html +186 -0
- package/src/scene/blocks/mk-specs-list/mk-specs-list.html +223 -0
- package/src/scene/blocks/news-ticker/news-ticker.html +409 -0
- package/src/scene/blocks/organic-light-leak-overlay/organic-light-leak-overlay.html +126 -0
- package/src/scene/blocks/oscilloscope-trace/oscilloscope-trace.html +519 -0
- package/src/scene/blocks/snow/snow.html +205 -0
- package/src/scene/blocks/split-flap-board/split-flap-board.html +347 -0
- package/src/scene/blocks/spotify-card/spotify-card.html +294 -0
- package/src/scene/blocks/starfield/starfield.html +188 -0
- package/src/scene/blocks/thread-message-stack/thread-message-stack.html +239 -0
- package/src/scene/blocks/week-in-merges/week-in-merges.html +470 -0
- package/src/scene/blocks/x-post/x-post.html +437 -0
- package/src/scene/blocks/yt-comment-card/yt-comment-card.html +319 -0
- package/src/scene/blocks/yt-lcd-background/yt-lcd-background.html +307 -0
- package/src/scene/blocks/yt-prism-title/yt-prism-title.html +205 -0
- package/src/scene/blocks.ts +229 -0
- package/src/scene/command.ts +272 -0
- package/src/scene/figure.ts +181 -0
- package/src/scene/glyphs.ts +107 -0
- package/src/scene/init.ts +86 -0
- package/src/scene/inspect/css-tree.d.ts +6 -0
- package/src/scene/inspect/page-scripts.ts +272 -0
- package/src/scene/inspect/report.ts +405 -0
- package/src/scene/inspect/run.ts +200 -0
- package/src/scene/inspect/source.ts +236 -0
- package/src/scene/label.ts +131 -0
- package/src/scene/net-guard.ts +74 -0
- package/src/scene/render.ts +292 -0
- package/src/scene/runtime/browser.ts +199 -0
- package/src/scene/runtime/core.ts +181 -0
- package/src/scene/runtime/react.tsx +115 -0
- package/src/scene/runtime/transitions.ts +133 -0
- package/src/scene/sanitize.ts +181 -0
- package/src/scene/scene.ts +152 -0
- package/src/scene/studio-command.ts +48 -0
- package/src/scene/studio-skin.ts +59 -0
- package/src/scene/studio.ts +337 -0
- package/src/scene/supply.ts +111 -0
- package/src/scene/timing.ts +288 -0
- package/src/scene/transitions/LICENSE +190 -0
- package/src/scene/transitions/NOTICE +5 -0
- package/src/scene/transitions/SOURCE.json +24 -0
- package/src/scene/transitions/transitions.global.js +38 -0
- package/src/scene/transitions.ts +26 -0
- package/src/service/all-projects.ts +153 -0
- package/src/service/commands.ts +256 -0
- package/src/service/due.ts +82 -0
- package/src/service/lock.ts +101 -0
- package/src/service/log-file.ts +56 -0
- package/src/service/state.ts +41 -0
- package/src/service/units.ts +90 -0
- package/src/service/worker.ts +51 -0
- package/src/skill-agents.ts +74 -0
- package/src/skill-install.ts +334 -0
- package/src/sync-write.ts +21 -0
- package/src/tick-report.ts +205 -0
- package/src/tts.ts +124 -0
- package/vendor/@vfrey/contracts/dist/account.d.ts +97 -0
- package/vendor/@vfrey/contracts/dist/account.js +27 -0
- package/vendor/@vfrey/contracts/dist/checks.d.ts +8 -0
- package/vendor/@vfrey/contracts/dist/checks.js +1 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.d.ts +4 -0
- package/vendor/@vfrey/contracts/dist/credentials/crypto.js +38 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.d.ts +10 -0
- package/vendor/@vfrey/contracts/dist/domain/advice-persona.js +24 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/backoff.js +27 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.d.ts +29 -0
- package/vendor/@vfrey/contracts/dist/domain/cadence.js +95 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.d.ts +146 -0
- package/vendor/@vfrey/contracts/dist/domain/creators.js +2 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.d.ts +127 -0
- package/vendor/@vfrey/contracts/dist/domain/disclosure.js +250 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.d.ts +34 -0
- package/vendor/@vfrey/contracts/dist/domain/errors.js +68 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.d.ts +1 -0
- package/vendor/@vfrey/contracts/dist/domain/redact.js +21 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/revenue.js +16 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/domain/safety.js +61 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.d.ts +13 -0
- package/vendor/@vfrey/contracts/dist/domain/similarity.js +100 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.d.ts +18 -0
- package/vendor/@vfrey/contracts/dist/domain/templates.js +67 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.d.ts +19 -0
- package/vendor/@vfrey/contracts/dist/domain/tiktok-chunks.js +52 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.d.ts +78 -0
- package/vendor/@vfrey/contracts/dist/domain/topics.js +4 -0
- package/vendor/@vfrey/contracts/dist/domain/types.d.ts +216 -0
- package/vendor/@vfrey/contracts/dist/domain/types.js +1 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.d.ts +57 -0
- package/vendor/@vfrey/contracts/dist/domain/variation.js +278 -0
- package/vendor/@vfrey/contracts/dist/events.d.ts +20 -0
- package/vendor/@vfrey/contracts/dist/events.js +1 -0
- package/vendor/@vfrey/contracts/dist/http.d.ts +11 -0
- package/vendor/@vfrey/contracts/dist/http.js +37 -0
- package/vendor/@vfrey/contracts/dist/index.d.ts +22 -0
- package/vendor/@vfrey/contracts/dist/index.js +22 -0
- package/vendor/@vfrey/contracts/dist/log.d.ts +2 -0
- package/vendor/@vfrey/contracts/dist/log.js +10 -0
- package/vendor/@vfrey/contracts/dist/mail.d.ts +55 -0
- package/vendor/@vfrey/contracts/dist/mail.js +1 -0
- package/vendor/@vfrey/contracts/package.json +18 -0
- package/vendor/@vfrey/ingest/dist/index.d.ts +41 -0
- package/vendor/@vfrey/ingest/dist/index.js +221 -0
- package/vendor/@vfrey/ingest/dist/parse.d.ts +5 -0
- package/vendor/@vfrey/ingest/dist/parse.js +165 -0
- package/vendor/@vfrey/ingest/package.json +21 -0
- package/vendor/framey/LICENSE +190 -0
- package/vendor/framey/NOTICE +15 -0
- package/vendor/framey/SOURCE.json +24 -0
- package/vendor/framey/STUDIO-SOURCE.json +9 -0
- package/vendor/framey/bin/framey-localize-fonts.mjs +12 -0
- package/vendor/framey/bin/framey.mjs +16 -0
- package/vendor/framey/dist/add-VMJD5KBS.js +75 -0
- package/vendor/framey/dist/animatedGifPrep-QPKGJ6PM.js +43 -0
- package/vendor/framey/dist/api.d.ts +211 -0
- package/vendor/framey/dist/api.js +76 -0
- package/vendor/framey/dist/appHistory-WW56FZTT.js +31 -0
- package/vendor/framey/dist/asset-WQKGOELE.js +26 -0
- package/vendor/framey/dist/audio-RSJWVWRA.js +1936 -0
- package/vendor/framey/dist/batchRender-3HPICDCT.js +366 -0
- package/vendor/framey/dist/beats-HL2BDUZI.js +273 -0
- package/vendor/framey/dist/benchmark-RWCWHI5Z.js +230 -0
- package/vendor/framey/dist/browser-ECYET3Q2.js +206 -0
- package/vendor/framey/dist/bundleWithLocalizedFonts-ALYBQEQT.js +52 -0
- package/vendor/framey/dist/captureCompositionFrame-4DA6SEH2.js +94 -0
- package/vendor/framey/dist/catalog-5JVDEUWK.js +979 -0
- package/vendor/framey/dist/catch-up-NEBCL5NO.js +106 -0
- package/vendor/framey/dist/check-F5N544ML.js +111 -0
- package/vendor/framey/dist/checkBrowser-LZ66CMJG.js +110 -0
- package/vendor/framey/dist/chunk-2K7Z2HVP.js +372 -0
- package/vendor/framey/dist/chunk-2UZQHDM7.js +323 -0
- package/vendor/framey/dist/chunk-2X4PQEYM.js +34 -0
- package/vendor/framey/dist/chunk-3422KMTD.js +168 -0
- package/vendor/framey/dist/chunk-3FYD4YEK.js +72 -0
- package/vendor/framey/dist/chunk-3GXQPCJC.js +682 -0
- package/vendor/framey/dist/chunk-3LM6YPBR.js +179 -0
- package/vendor/framey/dist/chunk-442JNXRM.js +20 -0
- package/vendor/framey/dist/chunk-4BNN5FGX.js +182 -0
- package/vendor/framey/dist/chunk-4P7NX5BM.js +11532 -0
- package/vendor/framey/dist/chunk-4UMPIPCQ.js +88 -0
- package/vendor/framey/dist/chunk-4YYJOFKS.js +28 -0
- package/vendor/framey/dist/chunk-54HJ63Q2.js +29 -0
- package/vendor/framey/dist/chunk-5ETJX6WM.js +89 -0
- package/vendor/framey/dist/chunk-5SG54JRS.js +68 -0
- package/vendor/framey/dist/chunk-634HSJ3J.js +58 -0
- package/vendor/framey/dist/chunk-66LREXGS.js +48 -0
- package/vendor/framey/dist/chunk-6BEFEWFB.js +25 -0
- package/vendor/framey/dist/chunk-6BNTGNGD.js +286 -0
- package/vendor/framey/dist/chunk-6DDPOHMK.js +140 -0
- package/vendor/framey/dist/chunk-6GV5XSOR.js +214 -0
- package/vendor/framey/dist/chunk-6JR2XUEE.js +127 -0
- package/vendor/framey/dist/chunk-73QVATIM.js +362 -0
- package/vendor/framey/dist/chunk-7FMUTGOH.js +279 -0
- package/vendor/framey/dist/chunk-7JQFJ6JJ.js +291 -0
- package/vendor/framey/dist/chunk-AHPGAESX.js +113 -0
- package/vendor/framey/dist/chunk-AHX34EFP.js +15 -0
- package/vendor/framey/dist/chunk-AJ5CSAEZ.js +487 -0
- package/vendor/framey/dist/chunk-B7RJV74O.js +5047 -0
- package/vendor/framey/dist/chunk-BL4U6U52.js +254 -0
- package/vendor/framey/dist/chunk-BSQ7XANM.js +210 -0
- package/vendor/framey/dist/chunk-BZ56MTDX.js +6 -0
- package/vendor/framey/dist/chunk-C5BO7PMT.js +282 -0
- package/vendor/framey/dist/chunk-CFUCHO4T.js +358 -0
- package/vendor/framey/dist/chunk-CZGHFATT.js +31 -0
- package/vendor/framey/dist/chunk-DLA44CMS.js +5649 -0
- package/vendor/framey/dist/chunk-DPVXBVLL.js +31292 -0
- package/vendor/framey/dist/chunk-DQBY7FRR.js +397 -0
- package/vendor/framey/dist/chunk-EAMLBAHA.js +154 -0
- package/vendor/framey/dist/chunk-EOVYQ3IG.js +143 -0
- package/vendor/framey/dist/chunk-EVMY7IPL.js +612 -0
- package/vendor/framey/dist/chunk-FG4BBFZI.js +122 -0
- package/vendor/framey/dist/chunk-FI4I7CME.js +56 -0
- package/vendor/framey/dist/chunk-FLREUEHW.js +2081 -0
- package/vendor/framey/dist/chunk-FUFP7A7S.js +1126 -0
- package/vendor/framey/dist/chunk-FV3PIKGH.js +56 -0
- package/vendor/framey/dist/chunk-FXMKHHPO.js +123 -0
- package/vendor/framey/dist/chunk-GGZAM5VH.js +1187 -0
- package/vendor/framey/dist/chunk-GKZEVJIB.js +39 -0
- package/vendor/framey/dist/chunk-GMECXFAN.js +57 -0
- package/vendor/framey/dist/chunk-GNEFDLFB.js +621 -0
- package/vendor/framey/dist/chunk-GPMLSOEG.js +19274 -0
- package/vendor/framey/dist/chunk-GPYVT5EE.js +59 -0
- package/vendor/framey/dist/chunk-H6IEMZEV.js +40 -0
- package/vendor/framey/dist/chunk-HEFQCIXR.js +170 -0
- package/vendor/framey/dist/chunk-HFQ4CWHA.js +118 -0
- package/vendor/framey/dist/chunk-HPDTCOKE.js +304 -0
- package/vendor/framey/dist/chunk-HTUHQKDB.js +548 -0
- package/vendor/framey/dist/chunk-HU2SPFNG.js +103 -0
- package/vendor/framey/dist/chunk-HVW7ABXD.js +19 -0
- package/vendor/framey/dist/chunk-IG7NDKXO.js +26 -0
- package/vendor/framey/dist/chunk-II3J3WXK.js +246 -0
- package/vendor/framey/dist/chunk-IR35BCLK.js +13 -0
- package/vendor/framey/dist/chunk-ISN5GRYI.js +20 -0
- package/vendor/framey/dist/chunk-IUONB55J.js +669 -0
- package/vendor/framey/dist/chunk-IVSUQR2B.js +161 -0
- package/vendor/framey/dist/chunk-IXU6PQSW.js +259 -0
- package/vendor/framey/dist/chunk-IZCW2NKT.js +29 -0
- package/vendor/framey/dist/chunk-JGXJU5PF.js +68 -0
- package/vendor/framey/dist/chunk-JHOCK3DG.js +27 -0
- package/vendor/framey/dist/chunk-JVBRJMOP.js +121 -0
- package/vendor/framey/dist/chunk-K4MJX7SR.js +17 -0
- package/vendor/framey/dist/chunk-K4WKGXRC.js +18 -0
- package/vendor/framey/dist/chunk-KGPLIH7B.js +127 -0
- package/vendor/framey/dist/chunk-KLIKCX6Q.js +63 -0
- package/vendor/framey/dist/chunk-KXTKKBOC.js +61 -0
- package/vendor/framey/dist/chunk-KZI44OPA.js +185 -0
- package/vendor/framey/dist/chunk-LEKZNRKI.js +45 -0
- package/vendor/framey/dist/chunk-LFGJWOVA.js +61 -0
- package/vendor/framey/dist/chunk-LLMSX6V6.js +17 -0
- package/vendor/framey/dist/chunk-LTGTN2TH.js +195 -0
- package/vendor/framey/dist/chunk-M2WBOQ2Q.js +54 -0
- package/vendor/framey/dist/chunk-MI5FTLYK.js +399 -0
- package/vendor/framey/dist/chunk-MLQP2QUF.js +1809 -0
- package/vendor/framey/dist/chunk-MMQ52NIZ.js +956 -0
- package/vendor/framey/dist/chunk-ND46XKK3.js +742 -0
- package/vendor/framey/dist/chunk-NYEXNSHM.js +504 -0
- package/vendor/framey/dist/chunk-O4YRB27L.js +57 -0
- package/vendor/framey/dist/chunk-O7BGSQ5F.js +92 -0
- package/vendor/framey/dist/chunk-OFQWTKPW.js +117 -0
- package/vendor/framey/dist/chunk-P5ZBLXJ6.js +91 -0
- package/vendor/framey/dist/chunk-P6LJ4Y32.js +4662 -0
- package/vendor/framey/dist/chunk-PKZ7RK5N.js +281 -0
- package/vendor/framey/dist/chunk-PURHAXFL.js +15 -0
- package/vendor/framey/dist/chunk-Q5X2B5WT.js +29 -0
- package/vendor/framey/dist/chunk-QAMKOG67.js +248 -0
- package/vendor/framey/dist/chunk-R6YBK4XX.js +514 -0
- package/vendor/framey/dist/chunk-RGLQURIX.js +2997 -0
- package/vendor/framey/dist/chunk-RNV3DM4S.js +3573 -0
- package/vendor/framey/dist/chunk-ROEW726S.js +4638 -0
- package/vendor/framey/dist/chunk-RQVDTPGD.js +6 -0
- package/vendor/framey/dist/chunk-RVZHGOGR.js +26 -0
- package/vendor/framey/dist/chunk-RXATE6FZ.js +385 -0
- package/vendor/framey/dist/chunk-S3S4TGEH.js +424 -0
- package/vendor/framey/dist/chunk-SDYDNHEE.js +11202 -0
- package/vendor/framey/dist/chunk-SSWBU67I.js +173 -0
- package/vendor/framey/dist/chunk-ST35C4K6.js +28 -0
- package/vendor/framey/dist/chunk-SZNTYTIV.js +990 -0
- package/vendor/framey/dist/chunk-T2EUWCJE.js +29 -0
- package/vendor/framey/dist/chunk-T6AGUTZJ.js +444 -0
- package/vendor/framey/dist/chunk-THJALHFJ.js +11131 -0
- package/vendor/framey/dist/chunk-TIQB63MX.js +18128 -0
- package/vendor/framey/dist/chunk-U242RTCT.js +134 -0
- package/vendor/framey/dist/chunk-UJZREAV6.js +3901 -0
- package/vendor/framey/dist/chunk-UMD3V7CU.js +264 -0
- package/vendor/framey/dist/chunk-UOFAE5RF.js +42 -0
- package/vendor/framey/dist/chunk-UTEIUGAZ.js +29 -0
- package/vendor/framey/dist/chunk-VAS6HQX3.js +138 -0
- package/vendor/framey/dist/chunk-VC76R6W2.js +27 -0
- package/vendor/framey/dist/chunk-VFZ7W5W7.js +226 -0
- package/vendor/framey/dist/chunk-VHNLZAE2.js +348 -0
- package/vendor/framey/dist/chunk-VMC4WSH5.js +278 -0
- package/vendor/framey/dist/chunk-VORQIH64.js +108 -0
- package/vendor/framey/dist/chunk-VTXFRIHO.js +186 -0
- package/vendor/framey/dist/chunk-W2LGJSEN.js +182 -0
- package/vendor/framey/dist/chunk-WKBM7NDG.js +32 -0
- package/vendor/framey/dist/chunk-WMQNIQDJ.js +2057 -0
- package/vendor/framey/dist/chunk-WN4XZC5F.js +504 -0
- package/vendor/framey/dist/chunk-WQ6ZXHFQ.js +133 -0
- package/vendor/framey/dist/chunk-X7HBSTCZ.js +53 -0
- package/vendor/framey/dist/chunk-X7MTZ4T2.js +6 -0
- package/vendor/framey/dist/chunk-XPTOJZJF.js +308 -0
- package/vendor/framey/dist/chunk-XT6MXC7A.js +6 -0
- package/vendor/framey/dist/chunk-XT7X6QCP.js +6 -0
- package/vendor/framey/dist/chunk-XTMDWNEC.js +2820 -0
- package/vendor/framey/dist/chunk-XUHOIDKG.js +6 -0
- package/vendor/framey/dist/chunk-XX7LTW5Y.js +138 -0
- package/vendor/framey/dist/chunk-Y3YGTZEG.js +382 -0
- package/vendor/framey/dist/chunk-Y5SBSGN7.js +346 -0
- package/vendor/framey/dist/chunk-YCMVI4MZ.js +625 -0
- package/vendor/framey/dist/chunk-YOR4CTCE.js +261 -0
- package/vendor/framey/dist/chunk-YPRBGITP.js +2748 -0
- package/vendor/framey/dist/chunk-ZKDMAOFU.js +32 -0
- package/vendor/framey/dist/chunk-ZW6DURPF.js +1318 -0
- package/vendor/framey/dist/clean-ZJOGDKDM.js +336 -0
- package/vendor/framey/dist/cli.js +439 -0
- package/vendor/framey/dist/cloudrun-HGJ3B6ZV.js +767 -0
- package/vendor/framey/dist/commands/contrast-audit.browser.js +643 -0
- package/vendor/framey/dist/commands/layout-audit.browser.js +2914 -0
- package/vendor/framey/dist/commands/motion-sample.browser.js +74 -0
- package/vendor/framey/dist/commands/motion-signature.browser.js +475 -0
- package/vendor/framey/dist/compare-W7UY3BAZ.js +463 -0
- package/vendor/framey/dist/compiler-TM3W6GYF.js +136 -0
- package/vendor/framey/dist/component-QF5VGYMU.js +176 -0
- package/vendor/framey/dist/compositions-XT3PMNDF.js +238 -0
- package/vendor/framey/dist/contactSheet-7W6GYHBQ.js +24 -0
- package/vendor/framey/dist/deploy-6FJPWKF2.js +299 -0
- package/vendor/framey/dist/destroy-HE2RIPKZ.js +57 -0
- package/vendor/framey/dist/deterministicFonts-RADD3BSX.js +50 -0
- package/vendor/framey/dist/dist-4S2FGGVY.js +422 -0
- package/vendor/framey/dist/dist-BE4B2EZE.js +32 -0
- package/vendor/framey/dist/dist-EWXJGN63.js +129 -0
- package/vendor/framey/dist/docs-VKIRFEEX.js +131 -0
- package/vendor/framey/dist/doctor-2UNJ2G7U.js +74 -0
- package/vendor/framey/dist/dom-37K6YOH4.js +14 -0
- package/vendor/framey/dist/dotEnv-7PZQTVIM.js +54 -0
- package/vendor/framey/dist/events-ACY3P2PA.js +76 -0
- package/vendor/framey/dist/events-IHB3B2MK.js +69 -0
- package/vendor/framey/dist/ffmpeg-DR5LGGSK.js +63 -0
- package/vendor/framey/dist/figma-BODVUPHB.js +63 -0
- package/vendor/framey/dist/fontData.generated-NXAOI4GH.js +13 -0
- package/vendor/framey/dist/fontLocalizeCli.js +142 -0
- package/vendor/framey/dist/gpuPolicy-XLEXORZY.js +49 -0
- package/vendor/framey/dist/grade-compare-GAXFQ74C.js +661 -0
- package/vendor/framey/dist/gsapParser-IEUSLYDJ.js +2885 -0
- package/vendor/framey/dist/gsapParserAcorn-LZJKCFQI.js +30 -0
- package/vendor/framey/dist/help-VCB5X2HS.js +184 -0
- package/vendor/framey/dist/history-ENGZEVFT.js +471 -0
- package/vendor/framey/dist/hyperframe.manifest.json +22 -0
- package/vendor/framey/dist/hyperframe.runtime.iife.js +1132 -0
- package/vendor/framey/dist/hyperframes-player.global.js +478 -0
- package/vendor/framey/dist/hyperframes-slideshow.global.js +148 -0
- package/vendor/framey/dist/info-WPCJHBMU.js +182 -0
- package/vendor/framey/dist/init-G4WGLYGD.js +1176 -0
- package/vendor/framey/dist/inspect-5IQ6CNEQ.js +106 -0
- package/vendor/framey/dist/installer-ML4HWUV7.js +40 -0
- package/vendor/framey/dist/keyframes-Q6XJO7VW.js +2755 -0
- package/vendor/framey/dist/lambda-A7JC74JF.js +468 -0
- package/vendor/framey/dist/layout-YET6XNXA.js +95 -0
- package/vendor/framey/dist/lint-I7FZN2VJ.js +162 -0
- package/vendor/framey/dist/lintProject-PDB3UN72.js +30 -0
- package/vendor/framey/dist/manager-CD6LJ7ZN.js +45 -0
- package/vendor/framey/dist/manager-WA4F2GST.js +67 -0
- package/vendor/framey/dist/media-treatment-BXYS4WVA.js +1030 -0
- package/vendor/framey/dist/media-use-LWH3MANK.js +113 -0
- package/vendor/framey/dist/models-K363FYM4.js +195 -0
- package/vendor/framey/dist/motionShot-GPAVWXLH.js +640 -0
- package/vendor/framey/dist/normalize-FASIPBUM.js +24 -0
- package/vendor/framey/dist/normalize-audio-VDWYRHN6.js +268 -0
- package/vendor/framey/dist/open-E3TB6P6J.js +128 -0
- package/vendor/framey/dist/parakeetRunner-UP34UOI2.js +58 -0
- package/vendor/framey/dist/pipeline-VXIUYEBU.js +64 -0
- package/vendor/framey/dist/play-BUTDQ4DV.js +353 -0
- package/vendor/framey/dist/policies-OLDYGFK5.js +309 -0
- package/vendor/framey/dist/preflight-2KZ2GWAZ.js +55 -0
- package/vendor/framey/dist/present-GXYSM7NX.js +353 -0
- package/vendor/framey/dist/preview-EKZ33JOJ.js +1910 -0
- package/vendor/framey/dist/producer-SKU5EV3J.js +13 -0
- package/vendor/framey/dist/progress-ZWB4Z4ET.js +76 -0
- package/vendor/framey/dist/projectConfig-3MZAYVTS.js +60 -0
- package/vendor/framey/dist/publication-DTKWNF74.js +33 -0
- package/vendor/framey/dist/remove-background-OY5L7XN2.js +265 -0
- package/vendor/framey/dist/render-CDV4GDFX.js +187 -0
- package/vendor/framey/dist/render-YE5YAMOD.js +2620 -0
- package/vendor/framey/dist/render-batch-ZAMFPCLL.js +274 -0
- package/vendor/framey/dist/renderSetupWorker.js +120 -0
- package/vendor/framey/dist/resolver-D5BIPSEW.js +42 -0
- package/vendor/framey/dist/runtimeVersion.js +18 -0
- package/vendor/framey/dist/sam-WFZXI4F7.js +23 -0
- package/vendor/framey/dist/shaderTransitionWorker.js +44 -0
- package/vendor/framey/dist/sherpa-RQWLV7I3.js +79 -0
- package/vendor/framey/dist/sherpaWorker.js +84 -0
- package/vendor/framey/dist/sites-AMOVABTK.js +54 -0
- package/vendor/framey/dist/skill-ZHVLH7KX.js +15 -0
- package/vendor/framey/dist/slideshow-SJGK4RQS.js +24 -0
- package/vendor/framey/dist/snapshot-TUXL6LBQ.js +1515 -0
- package/vendor/framey/dist/src-JHQGB2S6.js +215 -0
- package/vendor/framey/dist/src-Z5PGEMXB.js +532 -0
- package/vendor/framey/dist/state-B6H3WHCF.js +28 -0
- package/vendor/framey/dist/staticProjectServer-DFVZOUGI.js +63 -0
- package/vendor/framey/dist/studio/assets/hyperframes-player-DLaKjw0L.js +478 -0
- package/vendor/framey/dist/studio/assets/index-BX3KHhGX.js +71 -0
- package/vendor/framey/dist/studio/assets/index-CYbvOVp-.css +1 -0
- package/vendor/framey/dist/studio/assets/index-Dp3QEUGL.js +402 -0
- package/vendor/framey/dist/studio/assets/index-qrTAXbqc.js +531 -0
- package/vendor/framey/dist/studio/favicon.svg +14 -0
- package/vendor/framey/dist/studio/icons/timeline/audio.svg +7 -0
- package/vendor/framey/dist/studio/icons/timeline/captions.svg +5 -0
- package/vendor/framey/dist/studio/icons/timeline/composition.svg +12 -0
- package/vendor/framey/dist/studio/icons/timeline/image.svg +18 -0
- package/vendor/framey/dist/studio/icons/timeline/music.svg +10 -0
- package/vendor/framey/dist/studio/icons/timeline/text.svg +3 -0
- package/vendor/framey/dist/studio/index.html +23 -0
- package/vendor/framey/dist/studio-api-2NECGRMB.js +179 -0
- package/vendor/framey/dist/studioSelectionClient-IUUPKUHZ.js +25 -0
- package/vendor/framey/dist/studioServer-N2OPUMOQ.js +1569 -0
- package/vendor/framey/dist/synthesize-PKTB5ATG.js +183 -0
- package/vendor/framey/dist/system-7F3UEHQT.js +27 -0
- package/vendor/framey/dist/telemetry-DQGPEKM6.js +145 -0
- package/vendor/framey/dist/telemetry-UN55N6U6.js +77 -0
- package/vendor/framey/dist/timeline-LHUUMK63.js +1295 -0
- package/vendor/framey/dist/tokens-OLECWDFK.js +92 -0
- package/vendor/framey/dist/transcribe-O7HQKMIV.js +331 -0
- package/vendor/framey/dist/transcribe-UJESPBT5.js +76 -0
- package/vendor/framey/dist/tts-YCCQHZD4.js +234 -0
- package/vendor/framey/dist/urlDownloader-H2LXLG4U.js +48 -0
- package/vendor/framey/dist/usage-HG4WMM7A.js +2167 -0
- package/vendor/framey/dist/validate-F2XMN6EV.js +87 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/GSAP-LICENSE.txt +8 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/CustomEase.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/MotionPathPlugin.min.js +11 -0
- package/vendor/framey/dist/vendor/npm/gsap@3.15.0/dist/gsap.min.js +11 -0
- package/vendor/framey/dist/warmCheck.js +220 -0
- package/vendor/framey/dist/warmCheckClient.js +139 -0
- package/vendor/framey/dist/workspaceBuildCheck-XYD2YTFW.js +133 -0
- package/vendor/framey-player/LICENSE +190 -0
- package/vendor/framey-player/NOTICE +16 -0
- package/vendor/framey-player/SOURCE.json +6 -0
- package/vendor/framey-player/hyperframes-player.global.js +478 -0
- package/vendor/framey.pin +1 -0
- package/bin/index.js +0 -3
package/src/cloud/job.ts
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The contract between whatever queues a render and the render worker (spec 019, section C).
|
|
5
|
+
*
|
|
6
|
+
* A job is a folder (`/job/in` in the container) holding `job.json`, the scene folder and, optionally, the voice track
|
|
7
|
+
* that was synthesised before the container started (the cloud image has no `say`). The worker writes `video.mp4`,
|
|
8
|
+
* `transcript.txt`, `checks.json` and `result.json` into another folder (`/job/out`).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export const JOB_FILE = "job.json";
|
|
12
|
+
export const JOB_VERSION = 1;
|
|
13
|
+
export const JOB_ENGINES = ["vfrey-framey", "hyperframes", "remotion"] as const;
|
|
14
|
+
export type JobEngine = (typeof JOB_ENGINES)[number];
|
|
15
|
+
|
|
16
|
+
/** The most lines a job script may carry; the product's own limit for a script (src/generate/script.ts). */
|
|
17
|
+
export const JOB_LIMITS = { titleMax: 200, linesMax: 40, lineMax: 400 } as const;
|
|
18
|
+
|
|
19
|
+
export interface RenderJob {
|
|
20
|
+
version: 1;
|
|
21
|
+
engine: JobEngine;
|
|
22
|
+
/** The scene folder, relative to the input folder. `index.html` for the HTML engines, `index.tsx` for Remotion. */
|
|
23
|
+
scene: string;
|
|
24
|
+
script: { title: string; lines: string[] };
|
|
25
|
+
/** The voice track, relative to the input folder, already synthesised. Null renders a silent, estimated-timing video. */
|
|
26
|
+
voice: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export class JobError extends Error {
|
|
30
|
+
constructor(
|
|
31
|
+
message: string,
|
|
32
|
+
readonly code: string = "job_invalid",
|
|
33
|
+
) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = "JobError";
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* A path inside the input folder: relative, no `..`, no NUL. Returns the cleaned relative path.
|
|
41
|
+
* (Where the path resolves is checked again against the real folder at run time, so a symlink cannot lead out.)
|
|
42
|
+
*/
|
|
43
|
+
export function insidePath(value: unknown, field: string): string {
|
|
44
|
+
if (typeof value !== "string" || value.trim() === "") throw new JobError(`${field} must be a non-empty relative path.`);
|
|
45
|
+
if (value.includes("\0")) throw new JobError(`${field} must not contain a NUL byte.`);
|
|
46
|
+
if (path.isAbsolute(value) || /^[a-zA-Z]:/.test(value)) throw new JobError(`${field} must be relative to the input folder, not "${value}".`);
|
|
47
|
+
const clean = path.posix.normalize(value.split(path.sep).join("/"));
|
|
48
|
+
if (clean === ".." || clean.startsWith("../")) throw new JobError(`${field} must stay inside the input folder, not "${value}".`);
|
|
49
|
+
return clean === "." ? "." : clean;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Checks the parsed `job.json`. Throws a JobError naming the field; never returns a half-valid job. */
|
|
53
|
+
export function parseJob(raw: unknown): RenderJob {
|
|
54
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) throw new JobError("job.json must be a JSON object.");
|
|
55
|
+
const job = raw as Record<string, unknown>;
|
|
56
|
+
if (job.version !== JOB_VERSION) throw new JobError(`job.json version must be ${JOB_VERSION}, not ${JSON.stringify(job.version)}.`);
|
|
57
|
+
if (typeof job.engine !== "string" || !(JOB_ENGINES as readonly string[]).includes(job.engine)) {
|
|
58
|
+
throw new JobError(`engine must be one of ${JOB_ENGINES.join(", ")}, not ${JSON.stringify(job.engine)}.`);
|
|
59
|
+
}
|
|
60
|
+
const script = job.script as Record<string, unknown> | undefined;
|
|
61
|
+
if (!script || typeof script !== "object") throw new JobError("script must be an object with a title and lines.");
|
|
62
|
+
if (typeof script.title !== "string" || !script.title.trim() || script.title.length > JOB_LIMITS.titleMax) {
|
|
63
|
+
throw new JobError(`script.title must be 1 to ${JOB_LIMITS.titleMax} characters.`);
|
|
64
|
+
}
|
|
65
|
+
const lines = script.lines;
|
|
66
|
+
if (!Array.isArray(lines) || lines.length === 0 || lines.length > JOB_LIMITS.linesMax) {
|
|
67
|
+
throw new JobError(`script.lines must hold 1 to ${JOB_LIMITS.linesMax} lines.`);
|
|
68
|
+
}
|
|
69
|
+
for (const [i, line] of lines.entries()) {
|
|
70
|
+
if (typeof line !== "string" || !line.trim() || line.length > JOB_LIMITS.lineMax) throw new JobError(`script.lines[${i}] must be 1 to ${JOB_LIMITS.lineMax} characters.`);
|
|
71
|
+
}
|
|
72
|
+
const voice = job.voice === undefined || job.voice === null ? null : insidePath(job.voice, "voice");
|
|
73
|
+
return {
|
|
74
|
+
version: JOB_VERSION,
|
|
75
|
+
engine: job.engine as JobEngine,
|
|
76
|
+
scene: insidePath(job.scene, "scene"),
|
|
77
|
+
script: { title: script.title.trim(), lines: (lines as string[]).map((line) => line.trim()) },
|
|
78
|
+
voice,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type JobStatus = "succeeded" | "rejected" | "failed" | "timeout";
|
|
83
|
+
|
|
84
|
+
export interface CheckSummary {
|
|
85
|
+
id: string;
|
|
86
|
+
status: "pass" | "fail" | "skipped";
|
|
87
|
+
severity: string;
|
|
88
|
+
message: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** `result.json`. The worker writes every field except `timeout`, which only the executor can know. */
|
|
92
|
+
export interface RenderJobResult {
|
|
93
|
+
version: 1;
|
|
94
|
+
/** `succeeded`: the video exists and every check passed. `rejected`: it rendered but a check failed (do not publish). `failed`: no video. `timeout`: the executor killed it. */
|
|
95
|
+
status: JobStatus;
|
|
96
|
+
engine: JobEngine | null;
|
|
97
|
+
/** A stable code and a sentence for the person, set when the status is not `succeeded`. */
|
|
98
|
+
code: string | null;
|
|
99
|
+
message: string | null;
|
|
100
|
+
durations: { totalSec: number; renderSec: number | null; checksSec: number | null };
|
|
101
|
+
video: { width: number; height: number; durationSec: number; bytes: number } | null;
|
|
102
|
+
sizes: { videoBytes: number; transcriptBytes: number };
|
|
103
|
+
voice: "provided" | "none";
|
|
104
|
+
warnings: string[];
|
|
105
|
+
failedChecks: CheckSummary[];
|
|
106
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, rm, stat } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { unpackBundle } from "./bundle.js";
|
|
5
|
+
import type { FetchLike } from "./gcs-sign.js";
|
|
6
|
+
import type { RenderJobResult } from "./job.js";
|
|
7
|
+
import { runRenderJob, type RenderJobIo } from "./render-job.js";
|
|
8
|
+
|
|
9
|
+
type Env = Record<string, string | undefined>;
|
|
10
|
+
|
|
11
|
+
/** The variables the cloud executor sets. They are removed from the environment the scene's render sees. */
|
|
12
|
+
export const REMOTE_ENV = ["VFREY_JOB_IN_URL", "VFREY_JOB_OUT_URLS"] as const;
|
|
13
|
+
const OUTPUT_NAMES = ["video.mp4", "transcript.txt", "checks.json", "result.json"] as const;
|
|
14
|
+
|
|
15
|
+
/** A signed URL is only ever used if it points at Cloud Storage. */
|
|
16
|
+
function storageUrl(value: unknown, field: string): string {
|
|
17
|
+
if (typeof value !== "string") throw new Error(`${field} is not set.`);
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
if (url.protocol !== "https:" || url.hostname !== "storage.googleapis.com") throw new Error(`${field} must be an https://storage.googleapis.com URL.`);
|
|
20
|
+
return url.toString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* `vfrey render-job --remote`: the Cloud Run Job's command (spec 019, section C2). It downloads the input bundle through
|
|
25
|
+
* a signed URL, renders with the same code as the local container, and uploads each output through its signed URL. It holds
|
|
26
|
+
* no credentials: the URLs are the only access, and they are removed from the environment before the render starts.
|
|
27
|
+
*
|
|
28
|
+
* Exit 0 when the outputs (above all `result.json`) were uploaded, even if the scene failed: the result file says so, and
|
|
29
|
+
* a non-zero exit would only make Cloud Run retry. Exit 1 when a download or an upload failed, which the executor reads
|
|
30
|
+
* as an infrastructure failure.
|
|
31
|
+
*/
|
|
32
|
+
export async function runRemoteJob(io: { env: Env; stdout: (t: string) => void; stderr: (t: string) => void; fetch?: FetchLike; workRoot?: string; render?: (io: RenderJobIo) => Promise<RenderJobResult> }): Promise<number> {
|
|
33
|
+
const fetchImpl = io.fetch ?? fetch;
|
|
34
|
+
const work = await mkdtemp(path.join(io.workRoot ?? os.tmpdir(), "vfrey-remote-"));
|
|
35
|
+
try {
|
|
36
|
+
const inUrl = storageUrl(io.env.VFREY_JOB_IN_URL, "VFREY_JOB_IN_URL");
|
|
37
|
+
const outUrls = JSON.parse(io.env.VFREY_JOB_OUT_URLS ?? "null") as Record<string, string> | null;
|
|
38
|
+
if (!outUrls) throw new Error("VFREY_JOB_OUT_URLS is not set.");
|
|
39
|
+
const targets = Object.fromEntries(OUTPUT_NAMES.map((name) => [name, storageUrl(outUrls[name], `VFREY_JOB_OUT_URLS.${name}`)]));
|
|
40
|
+
|
|
41
|
+
const response = await fetchImpl(inUrl);
|
|
42
|
+
if (!response.ok) throw new Error(`Downloading the input bundle failed with HTTP ${response.status}.`);
|
|
43
|
+
const inputDir = path.join(work, "in");
|
|
44
|
+
const outputDir = path.join(work, "out");
|
|
45
|
+
await mkdir(outputDir, { recursive: true });
|
|
46
|
+
await unpackBundle(Buffer.from(await response.arrayBuffer()), inputDir);
|
|
47
|
+
|
|
48
|
+
const env: Env = { ...io.env };
|
|
49
|
+
for (const name of REMOTE_ENV) delete env[name];
|
|
50
|
+
const result = await (io.render ?? runRenderJob)({ inputDir, outputDir, env, stderr: io.stderr });
|
|
51
|
+
io.stdout(`${JSON.stringify({ status: result.status, code: result.code, message: result.message })}\n`);
|
|
52
|
+
|
|
53
|
+
// result.json goes last: when it exists, every other output already does.
|
|
54
|
+
for (const name of [...OUTPUT_NAMES].sort((a) => (a === "result.json" ? 1 : -1))) {
|
|
55
|
+
const file = path.join(outputDir, name);
|
|
56
|
+
if (!(await stat(file).catch(() => null))) continue;
|
|
57
|
+
const put = await fetchImpl(targets[name]!, { method: "PUT", body: new Uint8Array(await readFile(file)) });
|
|
58
|
+
if (!put.ok) throw new Error(`Uploading ${name} failed with HTTP ${put.status}.`);
|
|
59
|
+
}
|
|
60
|
+
return 0;
|
|
61
|
+
} catch (error) {
|
|
62
|
+
io.stderr(`✗ render-job --remote: ${error instanceof Error ? error.message : String(error)}\n`);
|
|
63
|
+
return 1;
|
|
64
|
+
} finally {
|
|
65
|
+
await rm(work, { recursive: true, force: true });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { copyFile, mkdir, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { linesToBeats } from "../generate/script.js";
|
|
5
|
+
import { realTools, reviewRendered, wordTotal, type CheckResult } from "../pipeline/review.js";
|
|
6
|
+
import { renderScene, type SceneEngine } from "../scene/render.js";
|
|
7
|
+
import { checkScene, SceneError, storeScene } from "../scene/scene.js";
|
|
8
|
+
import { JOB_FILE, JobError, parseJob, type CheckSummary, type RenderJob, type RenderJobResult } from "./job.js";
|
|
9
|
+
|
|
10
|
+
type Env = Record<string, string | undefined>;
|
|
11
|
+
|
|
12
|
+
export interface RenderJobIo {
|
|
13
|
+
/** The folder the job was mounted at. It is only read. */
|
|
14
|
+
inputDir: string;
|
|
15
|
+
/** The folder the outputs are written to. */
|
|
16
|
+
outputDir: string;
|
|
17
|
+
env?: Env;
|
|
18
|
+
stderr?: (text: string) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const round1 = (n: number) => Math.round(n * 10) / 10;
|
|
22
|
+
|
|
23
|
+
/** Resolves `rel` inside `root` and proves, with real paths, that it did not leave it (a symlink cannot lead out). */
|
|
24
|
+
async function resolveInside(root: string, rel: string, field: string): Promise<string> {
|
|
25
|
+
const base = await realpath(root);
|
|
26
|
+
const target = await realpath(path.resolve(base, rel)).catch(() => {
|
|
27
|
+
throw new JobError(`${field} "${rel}" does not exist in the input folder.`, "job_missing_file");
|
|
28
|
+
});
|
|
29
|
+
if (target !== base && !target.startsWith(base + path.sep)) throw new JobError(`${field} "${rel}" resolves outside the input folder.`, "job_path_escape");
|
|
30
|
+
return target;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function summarize(checks: CheckResult[]): CheckSummary[] {
|
|
34
|
+
return checks.filter((c) => c.status === "fail").map((c) => ({ id: c.id, status: c.status, severity: c.severity, message: c.message }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The worker's job: read `job.json`, render the scene with the product's own render path, run the after-render checks,
|
|
39
|
+
* and write the outputs. It makes no network request and reads no project or `~/.vfrey` state. A failure is reported in
|
|
40
|
+
* `result.json`; the returned result is what was written.
|
|
41
|
+
*/
|
|
42
|
+
export async function runRenderJob(io: RenderJobIo): Promise<RenderJobResult> {
|
|
43
|
+
const started = Date.now();
|
|
44
|
+
const env: Env = { ...(io.env ?? process.env) };
|
|
45
|
+
const log = io.stderr ?? (() => undefined);
|
|
46
|
+
const warnings: string[] = [];
|
|
47
|
+
const result: RenderJobResult = {
|
|
48
|
+
version: 1,
|
|
49
|
+
status: "failed",
|
|
50
|
+
engine: null,
|
|
51
|
+
code: null,
|
|
52
|
+
message: null,
|
|
53
|
+
durations: { totalSec: 0, renderSec: null, checksSec: null },
|
|
54
|
+
video: null,
|
|
55
|
+
sizes: { videoBytes: 0, transcriptBytes: 0 },
|
|
56
|
+
voice: "none",
|
|
57
|
+
warnings,
|
|
58
|
+
failedChecks: [],
|
|
59
|
+
};
|
|
60
|
+
const finish = async (): Promise<RenderJobResult> => {
|
|
61
|
+
result.durations.totalSec = round1((Date.now() - started) / 1000);
|
|
62
|
+
await mkdir(io.outputDir, { recursive: true });
|
|
63
|
+
await writeFile(path.join(io.outputDir, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
|
|
64
|
+
return result;
|
|
65
|
+
};
|
|
66
|
+
const fail = (status: "failed", code: string, message: string) => {
|
|
67
|
+
result.status = status;
|
|
68
|
+
result.code = code;
|
|
69
|
+
result.message = message;
|
|
70
|
+
};
|
|
71
|
+
const work = await mkdtemp(path.join(os.tmpdir(), "vfrey-job-"));
|
|
72
|
+
try {
|
|
73
|
+
let job: RenderJob;
|
|
74
|
+
try {
|
|
75
|
+
job = parseJob(JSON.parse(await readFile(path.join(io.inputDir, JOB_FILE), "utf8")));
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (error instanceof JobError) fail("failed", error.code, error.message);
|
|
78
|
+
else fail("failed", "job_unreadable", `job.json could not be read: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
return await finish();
|
|
80
|
+
}
|
|
81
|
+
result.engine = job.engine;
|
|
82
|
+
const transcript = `${job.script.lines.join("\n")}\n`;
|
|
83
|
+
await mkdir(io.outputDir, { recursive: true });
|
|
84
|
+
await writeFile(path.join(io.outputDir, "transcript.txt"), transcript);
|
|
85
|
+
result.sizes.transcriptBytes = Buffer.byteLength(transcript);
|
|
86
|
+
|
|
87
|
+
// A silent job: TTS_PROVIDER=none, or no voice in the job. The timing is then estimated from the script.
|
|
88
|
+
const silent = env.TTS_PROVIDER === "none" || job.voice === null;
|
|
89
|
+
let voicePath: string | null = null;
|
|
90
|
+
let sceneRoot: string;
|
|
91
|
+
try {
|
|
92
|
+
sceneRoot = await resolveInside(io.inputDir, job.scene, "scene");
|
|
93
|
+
if (!silent) voicePath = await resolveInside(io.inputDir, job.voice!, "voice");
|
|
94
|
+
if (voicePath && !(await stat(voicePath)).isFile()) throw new JobError("voice must be a file.", "job_missing_file");
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof JobError) {
|
|
97
|
+
fail("failed", error.code, error.message);
|
|
98
|
+
return await finish();
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
result.voice = voicePath ? "provided" : "none";
|
|
103
|
+
|
|
104
|
+
const rendered = path.join(work, "out", "video.mp4");
|
|
105
|
+
const renderStart = Date.now();
|
|
106
|
+
try {
|
|
107
|
+
// The scene is copied (and its SVGs sanitised, symlinks refused) before anything reads it.
|
|
108
|
+
await checkScene(sceneRoot, job.engine);
|
|
109
|
+
const stored = await storeScene(sceneRoot, path.join(work, "scene"));
|
|
110
|
+
warnings.push(...stored.warnings);
|
|
111
|
+
env.HOME = env.HOME || path.join(work, "home");
|
|
112
|
+
await renderScene({
|
|
113
|
+
engine: job.engine as SceneEngine,
|
|
114
|
+
sceneDir: path.join(work, "scene"),
|
|
115
|
+
title: job.script.title,
|
|
116
|
+
lines: job.script.lines,
|
|
117
|
+
beats: linesToBeats(job.script.lines).map((b) => ({ role: b.role, text: b.text, caption: b.caption })),
|
|
118
|
+
voiceoverPath: voicePath,
|
|
119
|
+
outputPath: rendered,
|
|
120
|
+
workDir: path.join(work, "render"),
|
|
121
|
+
env,
|
|
122
|
+
cwd: work,
|
|
123
|
+
warn: (message) => {
|
|
124
|
+
warnings.push(message);
|
|
125
|
+
log(`! ${message}\n`);
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
} catch (error) {
|
|
129
|
+
if (error instanceof SceneError) fail("failed", error.code, `${error.message} Fix: ${error.fix}`);
|
|
130
|
+
else fail("failed", "render_failed", `The render failed: ${error instanceof Error ? error.message : String(error)}`.slice(0, 600));
|
|
131
|
+
result.durations.renderSec = round1((Date.now() - renderStart) / 1000);
|
|
132
|
+
return await finish();
|
|
133
|
+
}
|
|
134
|
+
result.durations.renderSec = round1((Date.now() - renderStart) / 1000);
|
|
135
|
+
|
|
136
|
+
const checksStart = Date.now();
|
|
137
|
+
const checks = await reviewRendered({
|
|
138
|
+
file: rendered,
|
|
139
|
+
voicePath,
|
|
140
|
+
words: wordTotal(job.script.lines.join(" ")),
|
|
141
|
+
// The label is always burned by vfrey's render path for an agent-written scene.
|
|
142
|
+
labelBurned: true,
|
|
143
|
+
tools: realTools(env),
|
|
144
|
+
});
|
|
145
|
+
result.durations.checksSec = round1((Date.now() - checksStart) / 1000);
|
|
146
|
+
await writeFile(path.join(io.outputDir, "checks.json"), `${JSON.stringify(checks, null, 2)}\n`);
|
|
147
|
+
result.failedChecks = summarize(checks);
|
|
148
|
+
|
|
149
|
+
const dest = path.join(io.outputDir, "video.mp4");
|
|
150
|
+
await copyFile(rendered, dest);
|
|
151
|
+
const bytes = (await stat(dest)).size;
|
|
152
|
+
result.sizes.videoBytes = bytes;
|
|
153
|
+
const { probeVideo } = await import("../render/probe.js");
|
|
154
|
+
const probe = await probeVideo(dest, env);
|
|
155
|
+
result.video = probe ? { width: probe.width, height: probe.height, durationSec: probe.durationSec, bytes } : null;
|
|
156
|
+
if (result.failedChecks.length > 0) {
|
|
157
|
+
result.status = "rejected";
|
|
158
|
+
result.code = result.failedChecks[0]!.id;
|
|
159
|
+
result.message = `The finished video failed ${result.failedChecks.length} check${result.failedChecks.length === 1 ? "" : "s"}: ${result.failedChecks[0]!.message}`;
|
|
160
|
+
} else if (!probe) {
|
|
161
|
+
fail("failed", "video_unreadable", "The rendered file is not a readable video.");
|
|
162
|
+
} else {
|
|
163
|
+
result.status = "succeeded";
|
|
164
|
+
}
|
|
165
|
+
return await finish();
|
|
166
|
+
} finally {
|
|
167
|
+
await rm(work, { recursive: true, force: true });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** `vfrey render-job --input DIR --output DIR`: the container's command. Exit 0 only for a succeeded job; the result file has the rest. */
|
|
172
|
+
export async function runRenderJobCommand(argv: string[], io: { env: NodeJS.ProcessEnv; stdout: (t: string) => void; stderr: (t: string) => void }): Promise<number> {
|
|
173
|
+
if (argv.includes("--remote")) {
|
|
174
|
+
const { runRemoteJob } = await import("./remote-job.js");
|
|
175
|
+
return runRemoteJob(io);
|
|
176
|
+
}
|
|
177
|
+
const flag = (name: string) => {
|
|
178
|
+
const at = argv.indexOf(name);
|
|
179
|
+
const value = at === -1 ? undefined : argv[at + 1];
|
|
180
|
+
return value && !value.startsWith("--") ? value : null;
|
|
181
|
+
};
|
|
182
|
+
const input = flag("--input");
|
|
183
|
+
const output = flag("--output");
|
|
184
|
+
if (!input || !output) {
|
|
185
|
+
io.stderr("✗ render-job needs --input DIR and --output DIR.\nFix: run `vfrey render-job --input /job/in --output /job/out`.\n");
|
|
186
|
+
return 2;
|
|
187
|
+
}
|
|
188
|
+
const result = await runRenderJob({ inputDir: path.resolve(input), outputDir: path.resolve(output), env: io.env, stderr: io.stderr });
|
|
189
|
+
io.stdout(`${JSON.stringify({ status: result.status, code: result.code, message: result.message })}\n`);
|
|
190
|
+
return result.status === "succeeded" ? 0 : 1;
|
|
191
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { ConfigError } from "@vfrey/contracts";
|
|
3
|
+
import type { Account, AppConfig } from "@vfrey/contracts";
|
|
4
|
+
import { dataDirFor } from "./home.js";
|
|
5
|
+
|
|
6
|
+
type Env = Record<string, string | undefined>;
|
|
7
|
+
|
|
8
|
+
function requiredString(value: unknown, path: string): string {
|
|
9
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
10
|
+
throw new ConfigError("account_config", `${path} must be a non-empty string`);
|
|
11
|
+
}
|
|
12
|
+
return value.trim();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function optionalString(value: unknown): string | null {
|
|
16
|
+
if (typeof value !== "string" || value.trim() === "") return null;
|
|
17
|
+
return value.trim();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function stringList(value: unknown, path: string): string[] {
|
|
21
|
+
if (value === undefined) return [];
|
|
22
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) {
|
|
23
|
+
throw new ConfigError("account_config", `${path} must be a string array`);
|
|
24
|
+
}
|
|
25
|
+
return value.map((item) => item.trim()).filter(Boolean);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function rejectInlineSecrets(value: unknown, path: string): void {
|
|
29
|
+
if (Array.isArray(value)) {
|
|
30
|
+
value.forEach((item, index) => rejectInlineSecrets(item, `${path}[${index}]`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (!value || typeof value !== "object") return;
|
|
34
|
+
for (const [key, child] of Object.entries(value)) {
|
|
35
|
+
const childPath = `${path}.${key}`;
|
|
36
|
+
if (typeof child === "string") {
|
|
37
|
+
const secretKey = /token|secret|key/i.test(key);
|
|
38
|
+
const envName = key.endsWith("Env");
|
|
39
|
+
if (secretKey && !envName) {
|
|
40
|
+
throw new ConfigError(
|
|
41
|
+
"inline_secret",
|
|
42
|
+
`${childPath} looks like a credential field. Point it at an environment variable name ending in Env.`,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
if (/^(ya29\.|act\.|sk-|xox[bap]-)/.test(child)) {
|
|
46
|
+
throw new ConfigError("inline_secret", `${childPath} contains a credential value. Store it in the environment.`);
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
rejectInlineSecrets(child, childPath);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function parseAccounts(raw: unknown): Account[] {
|
|
55
|
+
rejectInlineSecrets(raw, "accounts");
|
|
56
|
+
const root = raw as { accounts?: unknown };
|
|
57
|
+
if (!root || !Array.isArray(root.accounts) || root.accounts.length === 0) {
|
|
58
|
+
throw new ConfigError("account_config", "accounts file must contain a non-empty accounts array");
|
|
59
|
+
}
|
|
60
|
+
return root.accounts.map((entry, index) => parseAccount(entry, index));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function parseAccount(entry: unknown, index: number): Account {
|
|
64
|
+
const path = `accounts[${index}]`;
|
|
65
|
+
const row = entry as Record<string, unknown>;
|
|
66
|
+
const cadence = (row.cadence ?? {}) as Record<string, unknown>;
|
|
67
|
+
const sources = (row.sources ?? {}) as Record<string, unknown>;
|
|
68
|
+
const platforms = (row.platforms ?? {}) as Record<string, unknown>;
|
|
69
|
+
const engage = (row.engage ?? {}) as Record<string, unknown>;
|
|
70
|
+
const voices = stringList(row.voices, `${path}.voices`);
|
|
71
|
+
if (voices.length === 0) {
|
|
72
|
+
throw new ConfigError("account_config", `${path}.voices needs at least one voice id`);
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
id: requiredString(row.id, `${path}.id`),
|
|
76
|
+
niche: requiredString(row.niche, `${path}.niche`),
|
|
77
|
+
language: optionalString(row.language) ?? "en",
|
|
78
|
+
cadence: {
|
|
79
|
+
timezone: requiredString(cadence.timezone, `${path}.cadence.timezone`),
|
|
80
|
+
minHoursBetweenPosts: numberOr(cadence.minHoursBetweenPosts, 18),
|
|
81
|
+
maxPostsPerDay: numberOr(cadence.maxPostsPerDay, 1),
|
|
82
|
+
windows: stringList(cadence.windows, `${path}.cadence.windows`),
|
|
83
|
+
},
|
|
84
|
+
voices,
|
|
85
|
+
hashtagPool: stringList(row.hashtagPool, `${path}.hashtagPool`),
|
|
86
|
+
sources: {
|
|
87
|
+
rss: stringList(sources.rss, `${path}.sources.rss`),
|
|
88
|
+
subreddits: stringList(sources.subreddits, `${path}.sources.subreddits`),
|
|
89
|
+
youtubeChannelIds: stringList(sources.youtubeChannelIds, `${path}.sources.youtubeChannelIds`),
|
|
90
|
+
youtubeTrendingRegion: optionalString(sources.youtubeTrendingRegion),
|
|
91
|
+
},
|
|
92
|
+
platforms: {
|
|
93
|
+
youtube: platforms.youtube ? parseYouTube(platforms.youtube, `${path}.platforms.youtube`) : undefined,
|
|
94
|
+
tiktok: platforms.tiktok ? parseTikTok(platforms.tiktok, `${path}.platforms.tiktok`) : undefined,
|
|
95
|
+
instagram: platforms.instagram ? parseInstagram(platforms.instagram, `${path}.platforms.instagram`) : undefined,
|
|
96
|
+
facebook: platforms.facebook ? parseFacebook(platforms.facebook, `${path}.platforms.facebook`) : undefined,
|
|
97
|
+
},
|
|
98
|
+
engage: {
|
|
99
|
+
enabled: engage.enabled !== false,
|
|
100
|
+
allowSpicy: engage.allowSpicy === true,
|
|
101
|
+
maxRepliesPerDay: numberOr(engage.maxRepliesPerDay, 20),
|
|
102
|
+
minSecondsBetweenReplies: numberOr(engage.minSecondsBetweenReplies, 45),
|
|
103
|
+
},
|
|
104
|
+
waiveAudioRotation: row.waiveAudioRotation === true,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function parseYouTube(value: unknown, path: string): Account["platforms"]["youtube"] {
|
|
109
|
+
const row = value as Record<string, unknown>;
|
|
110
|
+
const privacy = optionalString(row.privacyStatus) ?? "private";
|
|
111
|
+
if (privacy !== "public" && privacy !== "private" && privacy !== "unlisted") {
|
|
112
|
+
throw new ConfigError("account_config", `${path}.privacyStatus is invalid`);
|
|
113
|
+
}
|
|
114
|
+
return {
|
|
115
|
+
enabled: row.enabled !== false,
|
|
116
|
+
privacyStatus: privacy,
|
|
117
|
+
categoryId: optionalString(row.categoryId) ?? "22",
|
|
118
|
+
madeForKids: row.madeForKids === true,
|
|
119
|
+
refreshTokenEnv: requiredString(row.refreshTokenEnv, `${path}.refreshTokenEnv`),
|
|
120
|
+
clientIdEnv: requiredString(row.clientIdEnv, `${path}.clientIdEnv`),
|
|
121
|
+
clientSecretEnv: requiredString(row.clientSecretEnv, `${path}.clientSecretEnv`),
|
|
122
|
+
notifySubscribers: typeof row.notifySubscribers === "boolean" ? row.notifySubscribers : null,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseTikTok(value: unknown, path: string): Account["platforms"]["tiktok"] {
|
|
127
|
+
const row = value as Record<string, unknown>;
|
|
128
|
+
const privacy = optionalString(row.privacyLevel) ?? "SELF_ONLY";
|
|
129
|
+
const allowed = ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"];
|
|
130
|
+
if (!allowed.includes(privacy)) throw new ConfigError("account_config", `${path}.privacyLevel is invalid`);
|
|
131
|
+
return {
|
|
132
|
+
enabled: row.enabled !== false,
|
|
133
|
+
privacyLevel: privacy as "SELF_ONLY",
|
|
134
|
+
refreshTokenEnv: requiredString(row.refreshTokenEnv, `${path}.refreshTokenEnv`),
|
|
135
|
+
openIdEnv: requiredString(row.openIdEnv, `${path}.openIdEnv`),
|
|
136
|
+
brandContent: row.brandContent === true,
|
|
137
|
+
brandOrganic: row.brandOrganic === true,
|
|
138
|
+
disableComment: row.disableComment === true,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function parseInstagram(value: unknown, path: string): Account["platforms"]["instagram"] {
|
|
143
|
+
const row = value as Record<string, unknown>;
|
|
144
|
+
return {
|
|
145
|
+
enabled: row.enabled !== false,
|
|
146
|
+
igUserIdEnv: requiredString(row.igUserIdEnv, `${path}.igUserIdEnv`),
|
|
147
|
+
accessTokenEnv: requiredString(row.accessTokenEnv, `${path}.accessTokenEnv`),
|
|
148
|
+
shareToFeed: row.shareToFeed !== false,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseFacebook(value: unknown, path: string): Account["platforms"]["facebook"] {
|
|
153
|
+
const row = value as Record<string, unknown>;
|
|
154
|
+
return {
|
|
155
|
+
enabled: row.enabled !== false,
|
|
156
|
+
pageIdEnv: requiredString(row.pageIdEnv, `${path}.pageIdEnv`),
|
|
157
|
+
pageTokenEnv: requiredString(row.pageTokenEnv, `${path}.pageTokenEnv`),
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function numberOr(value: unknown, fallback: number): number {
|
|
162
|
+
if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
|
|
163
|
+
return value;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function loadConfig(env: Env, accountsJson?: string): AppConfig {
|
|
167
|
+
const accountsFile = env.ACCOUNTS_FILE || "accounts.json";
|
|
168
|
+
const raw = accountsJson ?? (existsSync(accountsFile) ? readFileSync(accountsFile, "utf8") : null);
|
|
169
|
+
const accounts = raw === null ? [] : parseAccounts(JSON.parse(raw) as unknown);
|
|
170
|
+
const provider = env.LLM_PROVIDER || "mock";
|
|
171
|
+
return {
|
|
172
|
+
appEnv: env.APP_ENV === "production" ? "production" : "qa",
|
|
173
|
+
dataDir: dataDirFor(env),
|
|
174
|
+
accountsFile,
|
|
175
|
+
dryRun: env.PIPELINE_DRY_RUN === "true",
|
|
176
|
+
llm: {
|
|
177
|
+
provider,
|
|
178
|
+
baseUrl: env.LLM_BASE_URL || defaultLlmBase(provider),
|
|
179
|
+
apiKey: env.LLM_API_KEY || null,
|
|
180
|
+
model: env.LLM_MODEL || defaultLlmModel(provider),
|
|
181
|
+
},
|
|
182
|
+
tts: {
|
|
183
|
+
provider: env.TTS_PROVIDER === "openai" ? "openai" : env.TTS_PROVIDER === "none" ? "none" : "local",
|
|
184
|
+
baseUrl: env.TTS_BASE_URL || "https://api.openai.com/v1",
|
|
185
|
+
apiKey: env.TTS_API_KEY || env.LLM_API_KEY || null,
|
|
186
|
+
model: env.TTS_MODEL || "tts-1",
|
|
187
|
+
} as AppConfig["tts"],
|
|
188
|
+
googleApiKey: env.YOUTUBE_API_KEY || null,
|
|
189
|
+
reddit: {
|
|
190
|
+
clientId: env.REDDIT_CLIENT_ID || null,
|
|
191
|
+
clientSecret: env.REDDIT_CLIENT_SECRET || null,
|
|
192
|
+
userAgent: env.REDDIT_USER_AGENT || "vfrey:v1 (by /u/operator)",
|
|
193
|
+
},
|
|
194
|
+
tiktok: {
|
|
195
|
+
clientKey: env.TIKTOK_CLIENT_KEY || null,
|
|
196
|
+
clientSecret: env.TIKTOK_CLIENT_SECRET || null,
|
|
197
|
+
clientAudited: env.TIKTOK_CLIENT_AUDITED === "true",
|
|
198
|
+
},
|
|
199
|
+
meta: {
|
|
200
|
+
graphVersion: env.META_GRAPH_VERSION || "v26.0",
|
|
201
|
+
appId: env.META_APP_ID || null,
|
|
202
|
+
appSecret: env.META_APP_SECRET || null,
|
|
203
|
+
},
|
|
204
|
+
youtube: {
|
|
205
|
+
dailyUploadCap: Number(env.YOUTUBE_DAILY_UPLOAD_CAP || "90"),
|
|
206
|
+
},
|
|
207
|
+
accounts,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function defaultLlmBase(provider: string): string {
|
|
212
|
+
switch (provider) {
|
|
213
|
+
case "openai":
|
|
214
|
+
return "https://api.openai.com/v1";
|
|
215
|
+
case "groq":
|
|
216
|
+
return "https://api.groq.com/openai/v1";
|
|
217
|
+
case "xai":
|
|
218
|
+
return "https://api.x.ai/v1";
|
|
219
|
+
case "gemini":
|
|
220
|
+
return "https://generativelanguage.googleapis.com/v1beta/openai";
|
|
221
|
+
case "anthropic":
|
|
222
|
+
return "https://api.anthropic.com";
|
|
223
|
+
default:
|
|
224
|
+
return "";
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function defaultLlmModel(provider: string): string {
|
|
229
|
+
switch (provider) {
|
|
230
|
+
case "anthropic":
|
|
231
|
+
return "claude-sonnet-5-5";
|
|
232
|
+
case "gemini":
|
|
233
|
+
return "gemini-2.5-flash";
|
|
234
|
+
case "groq":
|
|
235
|
+
return "llama-3.3-70b-versatile";
|
|
236
|
+
case "xai":
|
|
237
|
+
return "grok-4";
|
|
238
|
+
case "openai":
|
|
239
|
+
return "gpt-4.1-mini";
|
|
240
|
+
default:
|
|
241
|
+
return "mock";
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function envValue(env: Env, name: string): string {
|
|
246
|
+
const value = env[name];
|
|
247
|
+
if (!value) throw new ConfigError("missing_env", `Environment variable ${name} is not set`);
|
|
248
|
+
return value;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export interface Refusal {
|
|
252
|
+
message: string;
|
|
253
|
+
fix: string;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function platformEnvNames(account: Account): string[] {
|
|
257
|
+
const names: string[] = [];
|
|
258
|
+
const p = account.platforms;
|
|
259
|
+
if (p.youtube?.enabled) names.push(p.youtube.refreshTokenEnv, p.youtube.clientIdEnv, p.youtube.clientSecretEnv);
|
|
260
|
+
if (p.tiktok?.enabled) names.push(p.tiktok.refreshTokenEnv, p.tiktok.openIdEnv);
|
|
261
|
+
if (p.instagram?.enabled) names.push(p.instagram.igUserIdEnv, p.instagram.accessTokenEnv);
|
|
262
|
+
if (p.facebook?.enabled) names.push(p.facebook.pageIdEnv, p.facebook.pageTokenEnv);
|
|
263
|
+
return names;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The mock writer produces canned copy. Outside a dry run it must never feed a platform
|
|
268
|
+
* the operator has keys for. Returns the refusal, or null when the run may go on.
|
|
269
|
+
*/
|
|
270
|
+
export function mockWriterRefusal(config: AppConfig, env: Env, dryRun: boolean): Refusal | null {
|
|
271
|
+
if (dryRun) return null;
|
|
272
|
+
if (config.llm.provider !== "mock" && config.llm.provider !== "none") return null;
|
|
273
|
+
const keyed = config.accounts.some((account) => platformEnvNames(account).some((name) => Boolean(env[name])));
|
|
274
|
+
if (!keyed) return null;
|
|
275
|
+
return {
|
|
276
|
+
message: "LLM_PROVIDER is mock and a platform key is set. The mock writer would publish canned copy.",
|
|
277
|
+
fix: "Set LLM_PROVIDER and LLM_API_KEY to a real model, or run with --dry-run.",
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const WINDOW = /^(\d{2}):(\d{2})-(\d{2}):(\d{2})$/;
|
|
282
|
+
|
|
283
|
+
/** Returns a sentence naming the first fault in a cadence, or null when it is usable. */
|
|
284
|
+
export function validateCadence(cadence: {
|
|
285
|
+
timezone: string;
|
|
286
|
+
minHoursBetweenPosts: number;
|
|
287
|
+
maxPostsPerDay: number;
|
|
288
|
+
windows: string[];
|
|
289
|
+
}): string | null {
|
|
290
|
+
try {
|
|
291
|
+
new Intl.DateTimeFormat("en-US", { timeZone: cadence.timezone });
|
|
292
|
+
} catch {
|
|
293
|
+
return `timezone ${JSON.stringify(cadence.timezone)} is not a valid IANA timezone`;
|
|
294
|
+
}
|
|
295
|
+
if (!Number.isFinite(cadence.minHoursBetweenPosts) || cadence.minHoursBetweenPosts < 0) {
|
|
296
|
+
return "minHoursBetweenPosts must be a number of hours, 0 or more";
|
|
297
|
+
}
|
|
298
|
+
if (!Number.isFinite(cadence.maxPostsPerDay) || cadence.maxPostsPerDay < 0) {
|
|
299
|
+
return "maxPostsPerDay must be a number, 0 or more";
|
|
300
|
+
}
|
|
301
|
+
for (const window of cadence.windows) {
|
|
302
|
+
const match = WINDOW.exec(window);
|
|
303
|
+
const ok =
|
|
304
|
+
match !== null &&
|
|
305
|
+
Number(match[1]) < 24 && Number(match[3]) < 24 && Number(match[2]) < 60 && Number(match[4]) < 60;
|
|
306
|
+
if (!ok) return `window ${JSON.stringify(window)} must look like 09:00-17:30`;
|
|
307
|
+
}
|
|
308
|
+
return null;
|
|
309
|
+
}
|