@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.
- 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 +282 -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 +205 -0
- package/src/cli.ts +467 -0
- package/src/cloud/executor.ts +281 -0
- package/src/cloud/job.ts +106 -0
- package/src/cloud/render-job.ts +187 -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 +233 -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 +87 -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
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { mkdir, readFile, realpath, stat } from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import type { RenderJobResult } from "./job.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Executors run one render job in isolation (spec 019, section C). Agent-written scenes are untrusted code, and an
|
|
9
|
+
* executor is where they run: no network, a read-only root, resource limits, a wall-clock timeout.
|
|
10
|
+
*
|
|
11
|
+
* THE SEAM. The console's `RenderJob` queue (a Postgres table with a status and a lease) is what calls this:
|
|
12
|
+
* 1. a dispatcher leases a queued job and lays out two folders: an input folder (`job.json`, the scene, the voice
|
|
13
|
+
* that was synthesised before this step) and an empty output folder;
|
|
14
|
+
* 2. it calls `executor.run({ jobId, inputDir, outputDir })`;
|
|
15
|
+
* 3. it reads the outputs (`video.mp4`, `transcript.txt`, `checks.json`, `result.json`), uploads them, and records
|
|
16
|
+
* `renderSeconds` for metering and credits.
|
|
17
|
+
* Either the console calls this module in-process, or a thin worker process polls the queue and calls it. Nothing in
|
|
18
|
+
* here knows about the queue or the database. `local-docker` runs `docker run` on this machine; a `cloud-run-jobs`
|
|
19
|
+
* executor (slice C2) implements the same interface with Google Cloud Run Jobs.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export interface ExecutorJob {
|
|
23
|
+
/** A short id for the container name and the logs: letters, digits, dot, dash, underscore. */
|
|
24
|
+
jobId: string;
|
|
25
|
+
/** Absolute path of the folder with `job.json`. Mounted read-only. */
|
|
26
|
+
inputDir: string;
|
|
27
|
+
/** Absolute path of the folder the outputs are written to. Mounted read-write. */
|
|
28
|
+
outputDir: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ExecutorResult {
|
|
32
|
+
/** The worker's `result.json`, or a synthesised one when the container died, timed out, or wrote nothing. */
|
|
33
|
+
result: RenderJobResult;
|
|
34
|
+
/** Wall-clock seconds the container ran. This is what the job is metered by. */
|
|
35
|
+
renderSeconds: number;
|
|
36
|
+
exitCode: number | null;
|
|
37
|
+
timedOut: boolean;
|
|
38
|
+
/** The last lines the container printed to stderr, for a failure message. */
|
|
39
|
+
stderrTail: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface Executor {
|
|
43
|
+
readonly kind: string;
|
|
44
|
+
run(job: ExecutorJob): Promise<ExecutorResult>;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface LocalDockerOptions {
|
|
48
|
+
/** Image name and tag. Default: `VFREY_RENDER_IMAGE`, else `vfrey-render-worker:latest`. */
|
|
49
|
+
image?: string;
|
|
50
|
+
/** Wall-clock limit for one job, in seconds. The container is killed when it passes. Default 600. */
|
|
51
|
+
timeoutSec?: number;
|
|
52
|
+
cpus?: number;
|
|
53
|
+
/** Docker size string for the memory limit, for example `4g`. */
|
|
54
|
+
memory?: string;
|
|
55
|
+
/** Size of the /tmp tmpfs (the render's work folder). It counts against the memory limit. */
|
|
56
|
+
tmpfsSize?: string;
|
|
57
|
+
shmSize?: string;
|
|
58
|
+
pidsLimit?: number;
|
|
59
|
+
/** The user the container runs as, `uid` or `uid:gid`. Default: the caller's own, so the output folder is writable. */
|
|
60
|
+
user?: string;
|
|
61
|
+
/** Environment for the render, for example `TTS_PROVIDER=none`. Only these (and the fixed ones) reach the container. */
|
|
62
|
+
env?: Record<string, string>;
|
|
63
|
+
/** The docker binary. Default `docker`. */
|
|
64
|
+
docker?: string;
|
|
65
|
+
/** Replaces the worker command. For tests that probe the sandbox; the default is `vfrey render-job`. */
|
|
66
|
+
command?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const DEFAULT_IMAGE = "vfrey-render-worker:latest";
|
|
70
|
+
export const LIMIT_DEFAULTS = { timeoutSec: 600, cpus: 2, memory: "4g", tmpfsSize: "2g", shmSize: "256m", pidsLimit: 512 } as const;
|
|
71
|
+
|
|
72
|
+
export class ExecutorError extends Error {
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super(message);
|
|
75
|
+
this.name = "ExecutorError";
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const SIZE = /^[1-9]\d{0,5}[kmg]?$/i;
|
|
80
|
+
const NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/;
|
|
81
|
+
// Docker splits -v on ":", and a newline or NUL has no place in a path we mount.
|
|
82
|
+
const BAD_PATH_CHARS = /[:\0\r\n,]/;
|
|
83
|
+
const IMAGE = /^[a-z0-9][a-z0-9._\-/:@]{0,255}$/i;
|
|
84
|
+
|
|
85
|
+
/** A mount path: absolute, normalised, and free of the characters docker parses. */
|
|
86
|
+
export function mountPath(value: string, field: string): string {
|
|
87
|
+
if (!path.isAbsolute(value)) throw new ExecutorError(`${field} must be an absolute path, not "${value}".`);
|
|
88
|
+
if (BAD_PATH_CHARS.test(value)) throw new ExecutorError(`${field} must not contain ":", ",", or a control character.`);
|
|
89
|
+
const clean = path.resolve(value);
|
|
90
|
+
if (clean === path.parse(clean).root) throw new ExecutorError(`${field} must not be the filesystem root.`);
|
|
91
|
+
return clean;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function containerName(jobId: string): string {
|
|
95
|
+
if (!NAME.test(jobId)) throw new ExecutorError(`jobId must be letters, digits, ".", "-" or "_" (up to 64), not "${jobId}".`);
|
|
96
|
+
return `vfrey-render-${jobId}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function defaultUser(): string {
|
|
100
|
+
return typeof process.getuid === "function" ? `${process.getuid()}:${process.getgid?.() ?? process.getuid()}` : "10001:10001";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* The `docker run` argument list, without the docker binary. Pure: it reads nothing and runs nothing.
|
|
105
|
+
*
|
|
106
|
+
* --network none no network device but loopback; nothing leaves the container
|
|
107
|
+
* --read-only the root filesystem cannot be written; /tmp is a tmpfs, /job/out the only other write
|
|
108
|
+
* --cap-drop ALL, --security-opt no-new-privileges, --user <uid> no root, no capabilities, no privilege gain
|
|
109
|
+
* --cpus, --memory (swap = memory), --pids-limit, --shm-size, --tmpfs size resource limits
|
|
110
|
+
* -v <in>:/job/in:ro -v <out>:/job/out:rw the only host paths the container sees
|
|
111
|
+
*/
|
|
112
|
+
export function buildDockerArgs(job: ExecutorJob, options: LocalDockerOptions = {}): string[] {
|
|
113
|
+
const image = options.image ?? process.env.VFREY_RENDER_IMAGE ?? DEFAULT_IMAGE;
|
|
114
|
+
if (!IMAGE.test(image) || image.startsWith("-")) throw new ExecutorError(`The image name "${image}" is not valid.`);
|
|
115
|
+
const inputDir = mountPath(job.inputDir, "inputDir");
|
|
116
|
+
const outputDir = mountPath(job.outputDir, "outputDir");
|
|
117
|
+
if (inputDir === outputDir || inputDir.startsWith(outputDir + path.sep) || outputDir.startsWith(inputDir + path.sep)) {
|
|
118
|
+
throw new ExecutorError("inputDir and outputDir must be separate folders, neither inside the other.");
|
|
119
|
+
}
|
|
120
|
+
const name = containerName(job.jobId);
|
|
121
|
+
const cpus = options.cpus ?? LIMIT_DEFAULTS.cpus;
|
|
122
|
+
if (!(cpus > 0 && cpus <= 64)) throw new ExecutorError(`cpus must be between 0 and 64, not ${cpus}.`);
|
|
123
|
+
const memory = options.memory ?? LIMIT_DEFAULTS.memory;
|
|
124
|
+
const tmpfs = options.tmpfsSize ?? LIMIT_DEFAULTS.tmpfsSize;
|
|
125
|
+
const shm = options.shmSize ?? LIMIT_DEFAULTS.shmSize;
|
|
126
|
+
for (const [field, value] of [["memory", memory], ["tmpfsSize", tmpfs], ["shmSize", shm]] as const) {
|
|
127
|
+
if (!SIZE.test(value)) throw new ExecutorError(`${field} must be a size such as 512m or 4g, not "${value}".`);
|
|
128
|
+
}
|
|
129
|
+
const pids = options.pidsLimit ?? LIMIT_DEFAULTS.pidsLimit;
|
|
130
|
+
if (!Number.isInteger(pids) || pids < 16) throw new ExecutorError(`pidsLimit must be an integer of at least 16, not ${pids}.`);
|
|
131
|
+
const user = options.user ?? defaultUser();
|
|
132
|
+
if (!/^\d+(:\d+)?$/.test(user) || /^0(:|$)/.test(user)) throw new ExecutorError(`user must be a non-root numeric uid or uid:gid, not "${user}".`);
|
|
133
|
+
const env = { HOME: "/tmp/home", TMPDIR: "/tmp", ...(options.env ?? {}) };
|
|
134
|
+
const envArgs: string[] = [];
|
|
135
|
+
for (const [key, value] of Object.entries(env)) {
|
|
136
|
+
if (!/^[A-Z_][A-Z0-9_]*$/.test(key) || /[\0\r\n]/.test(value)) throw new ExecutorError(`The environment entry "${key}" is not valid.`);
|
|
137
|
+
envArgs.push("--env", `${key}=${value}`);
|
|
138
|
+
}
|
|
139
|
+
return [
|
|
140
|
+
"run",
|
|
141
|
+
"--rm",
|
|
142
|
+
"--pull", "never",
|
|
143
|
+
"--name", name,
|
|
144
|
+
"--init",
|
|
145
|
+
"--network", "none",
|
|
146
|
+
"--read-only",
|
|
147
|
+
"--tmpfs", `/tmp:rw,exec,nosuid,nodev,size=${tmpfs},mode=1777`,
|
|
148
|
+
"--shm-size", shm,
|
|
149
|
+
"--cpus", String(cpus),
|
|
150
|
+
"--memory", memory,
|
|
151
|
+
"--memory-swap", memory,
|
|
152
|
+
"--pids-limit", String(pids),
|
|
153
|
+
"--cap-drop", "ALL",
|
|
154
|
+
"--security-opt", "no-new-privileges",
|
|
155
|
+
"--user", user,
|
|
156
|
+
"-v", `${inputDir}:/job/in:ro`,
|
|
157
|
+
"-v", `${outputDir}:/job/out:rw`,
|
|
158
|
+
...envArgs,
|
|
159
|
+
image,
|
|
160
|
+
...(options.command ?? ["vfrey", "render-job", "--input", "/job/in", "--output", "/job/out"]),
|
|
161
|
+
];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function synthesized(status: "failed" | "timeout", code: string, message: string, seconds: number): RenderJobResult {
|
|
165
|
+
return {
|
|
166
|
+
version: 1,
|
|
167
|
+
status,
|
|
168
|
+
engine: null,
|
|
169
|
+
code,
|
|
170
|
+
message,
|
|
171
|
+
durations: { totalSec: seconds, renderSec: null, checksSec: null },
|
|
172
|
+
video: null,
|
|
173
|
+
sizes: { videoBytes: 0, transcriptBytes: 0 },
|
|
174
|
+
voice: "none",
|
|
175
|
+
warnings: [],
|
|
176
|
+
failedChecks: [],
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function run(binary: string, args: string[], timeoutMs: number): Promise<{ code: number | null; stdout: string; stderr: string }> {
|
|
181
|
+
return new Promise((resolve) => {
|
|
182
|
+
const child = spawn(binary, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
183
|
+
let stdout = "";
|
|
184
|
+
let stderr = "";
|
|
185
|
+
child.stdout.on("data", (c) => (stdout = (stdout + c).slice(-8000)));
|
|
186
|
+
child.stderr.on("data", (c) => (stderr = (stderr + c).slice(-8000)));
|
|
187
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), timeoutMs);
|
|
188
|
+
child.on("error", (error) => {
|
|
189
|
+
clearTimeout(timer);
|
|
190
|
+
resolve({ code: null, stdout, stderr: `${stderr}${error.message}` });
|
|
191
|
+
});
|
|
192
|
+
child.on("close", (code) => {
|
|
193
|
+
clearTimeout(timer);
|
|
194
|
+
resolve({ code, stdout, stderr });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Runs each job as one `docker run` on this machine. For development, tests, and a single-host deployment. */
|
|
200
|
+
export class LocalDockerExecutor implements Executor {
|
|
201
|
+
readonly kind = "local-docker";
|
|
202
|
+
constructor(private readonly options: LocalDockerOptions = {}) {}
|
|
203
|
+
|
|
204
|
+
async run(job: ExecutorJob): Promise<ExecutorResult> {
|
|
205
|
+
const docker = this.options.docker ?? "docker";
|
|
206
|
+
const timeoutSec = this.options.timeoutSec ?? LIMIT_DEFAULTS.timeoutSec;
|
|
207
|
+
if (!(timeoutSec > 0)) throw new ExecutorError(`timeoutSec must be positive, not ${timeoutSec}.`);
|
|
208
|
+
// Validate everything before touching the disk or docker.
|
|
209
|
+
buildDockerArgs(job, this.options);
|
|
210
|
+
const input = await stat(job.inputDir).catch(() => null);
|
|
211
|
+
if (!input?.isDirectory()) throw new ExecutorError(`inputDir "${job.inputDir}" is not a folder.`);
|
|
212
|
+
await mkdir(job.outputDir, { recursive: true });
|
|
213
|
+
// A symlinked mount path is resolved so the path that was validated is the path that is mounted.
|
|
214
|
+
const real = { ...job, inputDir: await realpath(job.inputDir), outputDir: await realpath(job.outputDir) };
|
|
215
|
+
const final = buildDockerArgs(real, this.options);
|
|
216
|
+
|
|
217
|
+
const name = containerName(job.jobId);
|
|
218
|
+
const started = Date.now();
|
|
219
|
+
let timedOut = false;
|
|
220
|
+
const outcome = await new Promise<{ code: number | null; stderr: string }>((resolve) => {
|
|
221
|
+
const child = spawn(docker, final, { stdio: ["ignore", "ignore", "pipe"] });
|
|
222
|
+
let stderr = "";
|
|
223
|
+
child.stderr.on("data", (c) => (stderr = (stderr + c).slice(-8000)));
|
|
224
|
+
const timer = setTimeout(async () => {
|
|
225
|
+
timedOut = true;
|
|
226
|
+
// Killing the container ends `docker run` too; --rm removes it. The client is killed as a backstop.
|
|
227
|
+
await run(docker, ["kill", name], 20_000);
|
|
228
|
+
await run(docker, ["rm", "-f", name], 20_000);
|
|
229
|
+
child.kill("SIGKILL");
|
|
230
|
+
}, timeoutSec * 1000);
|
|
231
|
+
child.on("error", (error) => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
resolve({ code: null, stderr: `${stderr}${error.message}` });
|
|
234
|
+
});
|
|
235
|
+
child.on("close", (code) => {
|
|
236
|
+
clearTimeout(timer);
|
|
237
|
+
resolve({ code, stderr });
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
const renderSeconds = Math.round(((Date.now() - started) / 1000) * 10) / 10;
|
|
241
|
+
const tail = outcome.stderr.trim().split(/\r?\n/).slice(-8).join("\n");
|
|
242
|
+
if (timedOut) {
|
|
243
|
+
// Make sure nothing is left running or on disk, whatever the kill raced with.
|
|
244
|
+
for (let attempt = 0; attempt < 20; attempt += 1) {
|
|
245
|
+
await run(docker, ["rm", "-f", name], 20_000);
|
|
246
|
+
if ((await run(docker, ["inspect", "--type", "container", name], 20_000)).code !== 0) break;
|
|
247
|
+
await new Promise((resolve) => setTimeout(resolve, 250));
|
|
248
|
+
}
|
|
249
|
+
return {
|
|
250
|
+
result: synthesized("timeout", "render_timeout", `The render ran past its ${timeoutSec} s limit and was stopped.`, renderSeconds),
|
|
251
|
+
renderSeconds,
|
|
252
|
+
exitCode: outcome.code,
|
|
253
|
+
timedOut: true,
|
|
254
|
+
stderrTail: tail,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
let parsed: RenderJobResult | null = null;
|
|
258
|
+
try {
|
|
259
|
+
parsed = JSON.parse(await readFile(path.join(job.outputDir, "result.json"), "utf8")) as RenderJobResult;
|
|
260
|
+
} catch {
|
|
261
|
+
parsed = null;
|
|
262
|
+
}
|
|
263
|
+
const result =
|
|
264
|
+
parsed ??
|
|
265
|
+
synthesized(
|
|
266
|
+
"failed",
|
|
267
|
+
outcome.code === 137 ? "render_oom" : "worker_crashed",
|
|
268
|
+
outcome.code === 137
|
|
269
|
+
? "The render was killed, most likely because it ran out of memory."
|
|
270
|
+
: `The worker exited with code ${outcome.code ?? "none"} and wrote no result.${tail ? ` ${tail.split("\n").slice(-2).join(" ")}` : ""}`,
|
|
271
|
+
renderSeconds,
|
|
272
|
+
);
|
|
273
|
+
return { result, renderSeconds, exitCode: outcome.code, timedOut: false, stderrTail: tail };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** The executor for `kind`. Only `local-docker` exists in this slice; `cloud-run-jobs` is slice C2. */
|
|
278
|
+
export function createExecutor(kind: string, options: LocalDockerOptions = {}): Executor {
|
|
279
|
+
if (kind === "local-docker") return new LocalDockerExecutor(options);
|
|
280
|
+
throw new ExecutorError(`There is no "${kind}" executor. Available: local-docker.`);
|
|
281
|
+
}
|
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,187 @@
|
|
|
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
|
+
const flag = (name: string) => {
|
|
174
|
+
const at = argv.indexOf(name);
|
|
175
|
+
const value = at === -1 ? undefined : argv[at + 1];
|
|
176
|
+
return value && !value.startsWith("--") ? value : null;
|
|
177
|
+
};
|
|
178
|
+
const input = flag("--input");
|
|
179
|
+
const output = flag("--output");
|
|
180
|
+
if (!input || !output) {
|
|
181
|
+
io.stderr("✗ render-job needs --input DIR and --output DIR.\nFix: run `vfrey render-job --input /job/in --output /job/out`.\n");
|
|
182
|
+
return 2;
|
|
183
|
+
}
|
|
184
|
+
const result = await runRenderJob({ inputDir: path.resolve(input), outputDir: path.resolve(output), env: io.env, stderr: io.stderr });
|
|
185
|
+
io.stdout(`${JSON.stringify({ status: result.status, code: result.code, message: result.message })}\n`);
|
|
186
|
+
return result.status === "succeeded" ? 0 : 1;
|
|
187
|
+
}
|