@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,642 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The quality gate. `reviewPackage()` runs a list of checks on a video before it is rendered, and `reviewRendered()`
|
|
3
|
+
* runs more on the finished file. Each check has a stable id, a severity, a plain message, and a Fix.
|
|
4
|
+
*
|
|
5
|
+
* Severity decides the job's outcome: any failed `compliance` check blocks the job (no retry); a failed `quality`
|
|
6
|
+
* check rejects it (after one automatic re-render when the check ran on a rendered file). See `gateOutcome()`.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from "node:child_process";
|
|
9
|
+
import { readdir, readFile, stat } from "node:fs/promises";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { promisify } from "node:util";
|
|
12
|
+
import {
|
|
13
|
+
COMPLIANCE_CHECKLIST,
|
|
14
|
+
buildFacebookFinish,
|
|
15
|
+
buildInstagramContainer,
|
|
16
|
+
buildTikTokInit,
|
|
17
|
+
buildYouTubeMetadata,
|
|
18
|
+
claimsAdvicePersona,
|
|
19
|
+
longestSharedWordRun,
|
|
20
|
+
} from "@vfrey/contracts";
|
|
21
|
+
import type { Account, ContentPackage, Platform } from "@vfrey/contracts";
|
|
22
|
+
import { SCRIPT_LIMITS } from "../generate/script.js";
|
|
23
|
+
import { labelPosition, labelSize } from "../scene/label.js";
|
|
24
|
+
import { sanitizeSvg } from "../scene/sanitize.js";
|
|
25
|
+
import { checkScene, SceneError } from "../scene/scene.js";
|
|
26
|
+
import type { RenderedVideo } from "../render/probe.js";
|
|
27
|
+
|
|
28
|
+
export type CheckSeverity = "quality" | "compliance";
|
|
29
|
+
export type CheckPhase = "before" | "after";
|
|
30
|
+
|
|
31
|
+
export interface CheckResult {
|
|
32
|
+
/** Stable. The run page, the tick summary, and `--json` all key on it. */
|
|
33
|
+
id: string;
|
|
34
|
+
phase: CheckPhase;
|
|
35
|
+
severity: CheckSeverity;
|
|
36
|
+
/** `skipped` means the check could not run (a tool is missing, or nothing was rendered). It never fails a job. */
|
|
37
|
+
status: "pass" | "fail" | "skipped";
|
|
38
|
+
message: string;
|
|
39
|
+
/** How to make a failed check pass. Null when the check passed. */
|
|
40
|
+
fix: string | null;
|
|
41
|
+
/** 1 for the first review of this phase; 2 after the automatic re-render. */
|
|
42
|
+
attempt: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Every threshold in one place, so the tests and the skill text quote the same numbers. */
|
|
46
|
+
export const THRESHOLDS = {
|
|
47
|
+
/** Words a voice reads per second. The same figure `linesToBeats` uses to time a beat. */
|
|
48
|
+
wordsPerSecond: 2.6,
|
|
49
|
+
/** How long the words take to say, before any voice exists. A short outside this is a stub or a lecture. */
|
|
50
|
+
spokenMinSec: 6,
|
|
51
|
+
spokenMaxSec: 75,
|
|
52
|
+
/** Measured pace on the finished video: words over the voice's real length. Outside this it races or drags. */
|
|
53
|
+
paceMinWps: 1.4,
|
|
54
|
+
paceMaxWps: 3.8,
|
|
55
|
+
/** Word count against the series' target length (`plan.durationTargetSec` at 2.6 words per second). */
|
|
56
|
+
lengthMinRatio: 0.25,
|
|
57
|
+
lengthMaxRatio: 1.8,
|
|
58
|
+
/** A scene file that can run script or touch the network is not shipped; the scene folder limit is in scene.ts. */
|
|
59
|
+
/** Rendered length against the voice length. */
|
|
60
|
+
durationToleranceSec: 2,
|
|
61
|
+
durationToleranceRatio: 0.15,
|
|
62
|
+
/** The picture is a 9:16 short. */
|
|
63
|
+
width: 1080,
|
|
64
|
+
height: 1920,
|
|
65
|
+
/** Black for more than this share of the video. */
|
|
66
|
+
blackShare: 0.5,
|
|
67
|
+
/** Frozen (no change between sampled frames) for more than this share of the video. */
|
|
68
|
+
frozenShare: 0.9,
|
|
69
|
+
/** Share of the label's border pixels that must be found where the label is burned. */
|
|
70
|
+
labelMatchShare: 0.4,
|
|
71
|
+
} as const;
|
|
72
|
+
|
|
73
|
+
const result = (partial: Omit<CheckResult, "attempt" | "fix"> & { fix?: string | null }, attempt: number): CheckResult => ({
|
|
74
|
+
fix: null,
|
|
75
|
+
...partial,
|
|
76
|
+
attempt,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
function pass(id: string, phase: CheckPhase, severity: CheckSeverity, message: string, attempt: number): CheckResult {
|
|
80
|
+
return result({ id, phase, severity, status: "pass", message }, attempt);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function fail(id: string, phase: CheckPhase, severity: CheckSeverity, message: string, fix: string, attempt: number): CheckResult {
|
|
84
|
+
return result({ id, phase, severity, status: "fail", message, fix }, attempt);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function skipped(id: string, phase: CheckPhase, severity: CheckSeverity, message: string, attempt: number): CheckResult {
|
|
88
|
+
return result({ id, phase, severity, status: "skipped", message }, attempt);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const wordCount = (text: string): number => text.split(/\s+/).filter(Boolean).length;
|
|
92
|
+
|
|
93
|
+
/** Words in a script, the same count the checks use. */
|
|
94
|
+
export const wordTotal = (script: string | null | undefined): number => wordCount(script ?? "");
|
|
95
|
+
|
|
96
|
+
// ------------------------------------------------------------------ script and copy
|
|
97
|
+
|
|
98
|
+
const PLACEHOLDER = /lorem ipsum|\bdolor sit amet\b|\bTODO\b|\bTBD\b|\bFIXME\b|\[(?:insert|your|add)[^\]]*\]|\{\{[^}]*\}\}|<(?:insert|your)[^>]*>|your (?:text|script|copy|title) here|\bxxx+\b/i;
|
|
99
|
+
|
|
100
|
+
/** Claims a short may not make: a promise of money, a cure, or no risk. Compliance, because the platforms act on them. */
|
|
101
|
+
export const BANNED_CLAIMS: Array<{ pattern: RegExp; label: string }> = [
|
|
102
|
+
{ pattern: /\bguaranteed? (?:returns?|profits?|income|results?|to (?:make|earn|win))\b/i, label: "a guaranteed return" },
|
|
103
|
+
{ pattern: /\b(?:get|getting) rich quick\b|\bmake \$?\d[\d,]*\s*(?:a|per|every) (?:day|week|month)\b/i, label: "an income promise" },
|
|
104
|
+
{ pattern: /\b(?:risk[- ]free|no risk at all|cannot lose|can't lose)\b/i, label: "a no-risk promise" },
|
|
105
|
+
{ pattern: /\b(?:cures?|cured|reverses?) (?:cancer|diabetes|alzheimer'?s|covid|depression|arthritis)\b/i, label: "a medical cure claim" },
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
export interface ScriptInput {
|
|
109
|
+
content: ContentPackage;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Pure. The copy and pace checks, run on the package before anything is rendered. */
|
|
113
|
+
export function checkScript(input: ScriptInput, attempt = 1): CheckResult[] {
|
|
114
|
+
const { content } = input;
|
|
115
|
+
const out: CheckResult[] = [];
|
|
116
|
+
const spoken = (content.voiceoverScript ?? "").split("\n");
|
|
117
|
+
const lines = spoken.map((line) => line.trim());
|
|
118
|
+
const words = wordCount(lines.join(" "));
|
|
119
|
+
|
|
120
|
+
const empties = lines.map((line, index) => (line ? -1 : index)).filter((index) => index >= 0);
|
|
121
|
+
if (words === 0 || empties.length > 0) {
|
|
122
|
+
out.push(
|
|
123
|
+
fail(
|
|
124
|
+
"copy.empty",
|
|
125
|
+
"before",
|
|
126
|
+
"quality",
|
|
127
|
+
words === 0 ? "The script has no spoken words." : `The script has ${empties.length} empty line${empties.length === 1 ? "" : "s"} (line ${empties.map((i) => i + 1).join(", ")}).`,
|
|
128
|
+
"write every line, or remove the empty ones, in the --script file.",
|
|
129
|
+
attempt,
|
|
130
|
+
),
|
|
131
|
+
);
|
|
132
|
+
} else {
|
|
133
|
+
out.push(pass("copy.empty", "before", "quality", "Every line has words.", attempt));
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const long = lines.map((line, index) => ({ index, length: line.length })).filter((item) => item.length > SCRIPT_LIMITS.lineMax);
|
|
137
|
+
out.push(
|
|
138
|
+
long.length > 0
|
|
139
|
+
? fail(
|
|
140
|
+
"copy.line-length",
|
|
141
|
+
"before",
|
|
142
|
+
"quality",
|
|
143
|
+
`Line ${long[0]!.index + 1} is ${long[0]!.length} characters; the limit is ${SCRIPT_LIMITS.lineMax}.`,
|
|
144
|
+
`split it into shorter spoken lines of ${SCRIPT_LIMITS.lineMax} characters or fewer.`,
|
|
145
|
+
attempt,
|
|
146
|
+
)
|
|
147
|
+
: pass("copy.line-length", "before", "quality", `No line is longer than ${SCRIPT_LIMITS.lineMax} characters.`, attempt),
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const target = content.plan?.durationTargetSec;
|
|
151
|
+
if (target && words > 0) {
|
|
152
|
+
const targetWords = target * THRESHOLDS.wordsPerSecond;
|
|
153
|
+
const min = Math.round(targetWords * THRESHOLDS.lengthMinRatio);
|
|
154
|
+
const max = Math.round(targetWords * THRESHOLDS.lengthMaxRatio);
|
|
155
|
+
out.push(
|
|
156
|
+
words < min || words > max
|
|
157
|
+
? fail(
|
|
158
|
+
"copy.length",
|
|
159
|
+
"before",
|
|
160
|
+
"quality",
|
|
161
|
+
`The script is ${words} words; this series' length target of ${target} s fits ${min} to ${max}.`,
|
|
162
|
+
words < min ? `add lines: about ${Math.round(targetWords)} words reads in ${target} s.` : `cut lines: about ${Math.round(targetWords)} words reads in ${target} s.`,
|
|
163
|
+
attempt,
|
|
164
|
+
)
|
|
165
|
+
: pass("copy.length", "before", "quality", `${words} words, inside ${min} to ${max} for a ${target} s target.`, attempt),
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const text = [content.title, content.hook, lines.join(" ")].join(" ");
|
|
170
|
+
const holder = PLACEHOLDER.exec(text);
|
|
171
|
+
out.push(
|
|
172
|
+
holder
|
|
173
|
+
? fail("copy.placeholder", "before", "quality", `The copy holds placeholder text ("${holder[0]}").`, "replace it with the real words in the --script file.", attempt)
|
|
174
|
+
: pass("copy.placeholder", "before", "quality", "No placeholder or lorem text.", attempt),
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const claim = BANNED_CLAIMS.find((item) => item.pattern.test(text));
|
|
178
|
+
const persona = claimsAdvicePersona(text);
|
|
179
|
+
const source = content.candidate ? `${content.candidate.title} ${content.candidate.excerpt}` : "";
|
|
180
|
+
const run = source ? longestSharedWordRun(lines.join(" "), source) : 0;
|
|
181
|
+
if (claim) {
|
|
182
|
+
out.push(fail("copy.claims", "before", "compliance", `The copy makes ${claim.label}, which platforms treat as a deceptive claim.`, "remove the claim or say what the evidence actually shows.", attempt));
|
|
183
|
+
} else if (persona) {
|
|
184
|
+
out.push(fail("copy.claims", "before", "compliance", "The script speaks as a doctor, lawyer, or financial adviser giving advice.", "rewrite it as information, not as a professional's advice.", attempt));
|
|
185
|
+
} else if (run >= 12) {
|
|
186
|
+
out.push(fail("copy.claims", "before", "compliance", `The script shares a ${run}-word run with its source; that is reused content.`, "put the idea in your own words.", attempt));
|
|
187
|
+
} else {
|
|
188
|
+
out.push(pass("copy.claims", "before", "compliance", "No banned claim, adviser persona, or copied source run.", attempt));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (words > 0) {
|
|
192
|
+
// The planned seconds are the template's band, not the voice's, so pace is judged on how long the words take to say.
|
|
193
|
+
const spokenSec = Math.round((words / THRESHOLDS.wordsPerSecond) * 10) / 10;
|
|
194
|
+
out.push(
|
|
195
|
+
spokenSec < THRESHOLDS.spokenMinSec || spokenSec > THRESHOLDS.spokenMaxSec
|
|
196
|
+
? fail(
|
|
197
|
+
"pace",
|
|
198
|
+
"before",
|
|
199
|
+
"quality",
|
|
200
|
+
`${words} words take about ${spokenSec} s to say at ${THRESHOLDS.wordsPerSecond} words per second; a short runs ${THRESHOLDS.spokenMinSec} to ${THRESHOLDS.spokenMaxSec} s.`,
|
|
201
|
+
spokenSec < THRESHOLDS.spokenMinSec ? `add lines: at least ${Math.ceil(THRESHOLDS.spokenMinSec * THRESHOLDS.wordsPerSecond)} words.` : `cut lines: at most ${Math.floor(THRESHOLDS.spokenMaxSec * THRESHOLDS.wordsPerSecond)} words.`,
|
|
202
|
+
attempt,
|
|
203
|
+
)
|
|
204
|
+
: pass("pace", "before", "quality", `About ${spokenSec} s of speech, inside ${THRESHOLDS.spokenMinSec} to ${THRESHOLDS.spokenMaxSec} s.`, attempt),
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// ------------------------------------------------------------------ disclosure
|
|
211
|
+
|
|
212
|
+
export interface DisclosureInput {
|
|
213
|
+
content: ContentPackage;
|
|
214
|
+
account: Pick<Account, "platforms" | "language">;
|
|
215
|
+
/** `vfrey` when vfrey overlays the label on the finished file (agent-written scenes); `template` when the built-in template draws it. */
|
|
216
|
+
label: "vfrey" | "template";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function platformProof(platform: Platform, content: ContentPackage, account: DisclosureInput["account"]): { value: unknown; field: string } | null {
|
|
220
|
+
try {
|
|
221
|
+
if (platform === "youtube") {
|
|
222
|
+
const settings = account.platforms.youtube;
|
|
223
|
+
const built = buildYouTubeMetadata({
|
|
224
|
+
title: content.title,
|
|
225
|
+
description: content.description,
|
|
226
|
+
tags: content.hashtags,
|
|
227
|
+
categoryId: settings?.categoryId ?? "27",
|
|
228
|
+
privacyStatus: settings?.privacyStatus ?? "private",
|
|
229
|
+
madeForKids: settings?.madeForKids ?? false,
|
|
230
|
+
language: account.language ?? "en",
|
|
231
|
+
});
|
|
232
|
+
return built.proof;
|
|
233
|
+
}
|
|
234
|
+
if (platform === "tiktok") {
|
|
235
|
+
const settings = account.platforms.tiktok;
|
|
236
|
+
return buildTikTokInit({
|
|
237
|
+
caption: content.caption,
|
|
238
|
+
privacyLevel: "SELF_ONLY",
|
|
239
|
+
disableComment: settings?.disableComment ?? false,
|
|
240
|
+
coverTimestampMs: 0,
|
|
241
|
+
brandContent: settings?.brandContent ?? false,
|
|
242
|
+
brandOrganic: settings?.brandOrganic ?? false,
|
|
243
|
+
videoSize: 1,
|
|
244
|
+
chunkSize: 1,
|
|
245
|
+
totalChunkCount: 1,
|
|
246
|
+
audited: false,
|
|
247
|
+
privacyOptions: ["SELF_ONLY"],
|
|
248
|
+
}).proof;
|
|
249
|
+
}
|
|
250
|
+
if (platform === "instagram") return buildInstagramContainer({ caption: content.caption, shareToFeed: account.platforms.instagram?.shareToFeed ?? true }).proof;
|
|
251
|
+
return buildFacebookFinish({ videoId: "check", title: content.title, description: content.description }).proof;
|
|
252
|
+
} catch {
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Pure. The label will be burned, and every platform the series publishes to carries its AI-content field. */
|
|
258
|
+
export function checkDisclosure(input: DisclosureInput, attempt = 1): CheckResult[] {
|
|
259
|
+
const out: CheckResult[] = [];
|
|
260
|
+
out.push(
|
|
261
|
+
pass(
|
|
262
|
+
"disclosure.label",
|
|
263
|
+
"before",
|
|
264
|
+
"compliance",
|
|
265
|
+
input.label === "vfrey" ? "vfrey burns the AI-GENERATED label on the finished video." : "The AI-GENERATED label is part of the built-in template, on every frame.",
|
|
266
|
+
attempt,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
const enabled = (["youtube", "tiktok", "instagram", "facebook"] as Platform[]).filter((platform) => input.account.platforms[platform]?.enabled);
|
|
270
|
+
if (enabled.length === 0) {
|
|
271
|
+
out.push(skipped("disclosure.platform", "before", "compliance", "No platform is enabled, so there is no platform field to set.", attempt));
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
const missing: string[] = [];
|
|
275
|
+
for (const platform of enabled) {
|
|
276
|
+
const wanted = COMPLIANCE_CHECKLIST.find((item) => item.platform === platform);
|
|
277
|
+
const proof = platformProof(platform, input.content, input.account);
|
|
278
|
+
if (!proof || proof.value !== true || (wanted && proof.field !== wanted.field)) missing.push(wanted?.field ? `${platform} (${wanted.field})` : platform);
|
|
279
|
+
}
|
|
280
|
+
out.push(
|
|
281
|
+
missing.length > 0
|
|
282
|
+
? fail(
|
|
283
|
+
"disclosure.platform",
|
|
284
|
+
"before",
|
|
285
|
+
"compliance",
|
|
286
|
+
`The platform AI-content field would not be set for ${missing.join(", ")}.`,
|
|
287
|
+
"do not publish there until vfrey sets the field; run `npx vfrey doctor` and update vfrey.",
|
|
288
|
+
attempt,
|
|
289
|
+
)
|
|
290
|
+
: pass("disclosure.platform", "before", "compliance", `The AI-content field is set for ${enabled.join(", ")}.`, attempt),
|
|
291
|
+
);
|
|
292
|
+
return out;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ------------------------------------------------------------------ scene
|
|
296
|
+
|
|
297
|
+
const TEXT_EXT = new Set([".html", ".htm", ".css", ".js", ".mjs", ".ts", ".tsx", ".jsx", ".svg"]);
|
|
298
|
+
/** The pinned GSAP script tags vfrey rewrites to its local copy. They are the one allowed address. */
|
|
299
|
+
const GSAP_TAG = /<script\b[^>]*\bsrc\s*=\s*["']https?:\/\/(?:cdn\.jsdelivr\.net\/npm\/gsap@[^"']*|cdnjs\.cloudflare\.com\/ajax\/libs\/gsap\/[^"']*|unpkg\.com\/gsap[^"']*|code\.jquery\.com\/[^"']*gsap[^"']*)["'][^>]*>\s*<\/script>/gi;
|
|
300
|
+
const EXTERNAL = /(?:\b(?:src|href|poster|data|action)\s*=\s*["']\s*(?:https?:)?\/\/[^"']+|url\(\s*["']?\s*(?:https?:)?\/\/[^)"']+|@import\s+(?:url\()?["']?\s*(?:https?:)?\/\/[^"')]+|\bfrom\s+["']https?:\/\/[^"']+|\bimport\(\s*["']https?:\/\/[^"']+|\bfetch\(\s*["']https?:\/\/[^"']+|\bnew\s+(?:WebSocket|EventSource)\(\s*["']\s*(?:https?|wss?):\/\/[^"']+)/i;
|
|
301
|
+
|
|
302
|
+
async function sceneFiles(dir: string, base = dir): Promise<string[]> {
|
|
303
|
+
const out: string[] = [];
|
|
304
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
305
|
+
if (entry.name === "node_modules" || entry.name === ".git" || (dir === base && entry.name === "vfrey")) continue;
|
|
306
|
+
const abs = path.join(dir, entry.name);
|
|
307
|
+
if (entry.isDirectory()) out.push(...(await sceneFiles(abs, base)));
|
|
308
|
+
else if (entry.isFile()) out.push(abs);
|
|
309
|
+
}
|
|
310
|
+
return out;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
export interface SceneInput {
|
|
314
|
+
dir: string;
|
|
315
|
+
engineId: string;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The scene checks. They read the folder, so they are async; each failure names the file. */
|
|
319
|
+
export async function checkSceneFolder(input: SceneInput, attempt = 1): Promise<CheckResult[]> {
|
|
320
|
+
const out: CheckResult[] = [];
|
|
321
|
+
try {
|
|
322
|
+
const info = await checkScene(input.dir, input.engineId);
|
|
323
|
+
out.push(pass("scene.entry", "before", "quality", `The scene has its ${info.kind === "html" ? "index.html" : "index.tsx"} for this engine.`, attempt));
|
|
324
|
+
out.push(pass("scene.size", "before", "quality", `${info.files} files, ${(info.bytes / 1024 / 1024).toFixed(1)} MB, inside the limit.`, attempt));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (!(error instanceof SceneError)) throw error;
|
|
327
|
+
const id = error.code === "scene_too_large" ? "scene.size" : "scene.entry";
|
|
328
|
+
out.push(fail(id, "before", "quality", error.message, error.fix, attempt));
|
|
329
|
+
if (id === "scene.entry") return out;
|
|
330
|
+
out.push(pass("scene.entry", "before", "quality", "The scene has its entry file.", attempt));
|
|
331
|
+
}
|
|
332
|
+
const files = await sceneFiles(input.dir).catch(() => [] as string[]);
|
|
333
|
+
const badSvg: string[] = [];
|
|
334
|
+
const stripped: string[] = [];
|
|
335
|
+
const external: string[] = [];
|
|
336
|
+
for (const file of files) {
|
|
337
|
+
const ext = path.extname(file).toLowerCase();
|
|
338
|
+
const rel = path.relative(input.dir, file).split(path.sep).join("/");
|
|
339
|
+
if (!TEXT_EXT.has(ext)) continue;
|
|
340
|
+
if ((await stat(file)).size > 8 * 1024 * 1024) continue;
|
|
341
|
+
const text = await readFile(file, "utf8");
|
|
342
|
+
if (ext === ".svg") {
|
|
343
|
+
const checked = sanitizeSvg(text);
|
|
344
|
+
if (!checked.ok) badSvg.push(`${rel}: ${checked.message}`);
|
|
345
|
+
else if (checked.removed.length > 0) stripped.push(rel);
|
|
346
|
+
}
|
|
347
|
+
const found = EXTERNAL.exec(text.replace(GSAP_TAG, "").replace(/xmlns(?::\w+)?\s*=\s*["'][^"']*["']/gi, ""));
|
|
348
|
+
if (found) external.push(`${rel} (${found[0].slice(0, 60).replace(/\s+/g, " ")})`);
|
|
349
|
+
}
|
|
350
|
+
out.push(
|
|
351
|
+
badSvg.length > 0
|
|
352
|
+
? fail("scene.svg", "before", "quality", `An SVG cannot be used: ${badSvg[0]}.`, "redraw that SVG without entities or scripts, or remove it from the scene folder.", attempt)
|
|
353
|
+
: pass("scene.svg", "before", "quality", stripped.length > 0 ? `SVGs are clean; unsafe parts were stripped from ${stripped.join(", ")}.` : "Every SVG passed the sanitizer.", attempt),
|
|
354
|
+
);
|
|
355
|
+
// A scene that reaches for the network is not refused: a render has no network, so the request fails fast and the
|
|
356
|
+
// run records a warning (see net-guard.ts). The check passes and says so, so the run page shows it.
|
|
357
|
+
out.push(
|
|
358
|
+
pass(
|
|
359
|
+
"scene.external-url",
|
|
360
|
+
"before",
|
|
361
|
+
"quality",
|
|
362
|
+
external.length > 0
|
|
363
|
+
? `The scene points at the network (${external[0]}). A render has no network, so it will not load; the run records a warning. Put the file in the scene folder to use it.`
|
|
364
|
+
: "The scene loads nothing from the network.",
|
|
365
|
+
attempt,
|
|
366
|
+
),
|
|
367
|
+
);
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
// ------------------------------------------------------------------ reviewPackage
|
|
372
|
+
|
|
373
|
+
export interface ReviewPackageInput {
|
|
374
|
+
content: ContentPackage;
|
|
375
|
+
account: Pick<Account, "platforms" | "language">;
|
|
376
|
+
/** The agent-written scene the render will use, or null for the default template. */
|
|
377
|
+
scene: SceneInput | null;
|
|
378
|
+
attempt?: number;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/** Everything that can be checked before a render. Scene checks run only when there is a scene folder to read. */
|
|
382
|
+
export async function reviewPackage(input: ReviewPackageInput): Promise<CheckResult[]> {
|
|
383
|
+
const attempt = input.attempt ?? 1;
|
|
384
|
+
const out = [...checkScript({ content: input.content }, attempt)];
|
|
385
|
+
if (input.scene) out.push(...(await checkSceneFolder(input.scene, attempt)));
|
|
386
|
+
out.push(...checkDisclosure({ content: input.content, account: input.account, label: input.scene ? "vfrey" : "template" }, attempt));
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// ------------------------------------------------------------------ after the render
|
|
391
|
+
|
|
392
|
+
/** What the after-render checks read from the file. Tests pass fakes; the pipeline passes `realTools`. */
|
|
393
|
+
export interface ReviewTools {
|
|
394
|
+
/** False when ffprobe or ffmpeg is not installed: the after-render checks are skipped, not failed. */
|
|
395
|
+
available(): Promise<boolean>;
|
|
396
|
+
probe(file: string): Promise<RenderedVideo | null>;
|
|
397
|
+
voiceSeconds(file: string): Promise<number | null>;
|
|
398
|
+
/** Seconds black, seconds frozen, from a sampled pass. Null when the pass failed. */
|
|
399
|
+
sampleMotion(file: string, durationSec: number): Promise<{ blackSec: number; frozenSec: number } | null>;
|
|
400
|
+
/** The share of the label's border colour found where the label is burned (0 to 1). Null when it could not be read. */
|
|
401
|
+
labelMatch(file: string, atSec: number): Promise<number | null>;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export interface ReviewRenderedInput {
|
|
405
|
+
file: string;
|
|
406
|
+
/** The voice file, when there is one. The video should be as long as the voice. */
|
|
407
|
+
voicePath: string | null;
|
|
408
|
+
/** Words in the script, for the measured pace. */
|
|
409
|
+
words?: number;
|
|
410
|
+
/** True when vfrey burned the label (an agent-written scene). Template renders draw it inside the picture. */
|
|
411
|
+
labelBurned: boolean;
|
|
412
|
+
tools: ReviewTools;
|
|
413
|
+
attempt?: number;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** True when the rendered video could pass every after-render check; `fail` results say why not. */
|
|
417
|
+
export async function reviewRendered(input: ReviewRenderedInput): Promise<CheckResult[]> {
|
|
418
|
+
const attempt = input.attempt ?? 1;
|
|
419
|
+
const { tools } = input;
|
|
420
|
+
if (!(await tools.available())) {
|
|
421
|
+
return ["frames.readable", "frames.duration", "pace.measured", "frames.resolution", "frames.black", "frames.frozen", "label.present"].map((id) =>
|
|
422
|
+
skipped(id, "after", id === "label.present" ? "compliance" : "quality", "ffprobe and ffmpeg are not installed, so the rendered file was not inspected.", attempt),
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const out: CheckResult[] = [];
|
|
426
|
+
const probe = await tools.probe(input.file);
|
|
427
|
+
if (!probe) {
|
|
428
|
+
out.push(fail("frames.readable", "after", "quality", "The rendered file is not a readable video.", "render again; if it repeats, run `npx vfrey doctor` and check the engine.", attempt));
|
|
429
|
+
out.push(skipped("label.present", "after", "compliance", "There is no readable video to look for the label in.", attempt));
|
|
430
|
+
return out;
|
|
431
|
+
}
|
|
432
|
+
out.push(pass("frames.readable", "after", "quality", `A ${probe.width}×${probe.height} video of ${probe.durationSec} s.`, attempt));
|
|
433
|
+
|
|
434
|
+
const voice = input.voicePath ? await tools.voiceSeconds(input.voicePath) : null;
|
|
435
|
+
if (voice) {
|
|
436
|
+
const allowed = Math.max(THRESHOLDS.durationToleranceSec, voice * THRESHOLDS.durationToleranceRatio);
|
|
437
|
+
const gap = Math.abs(probe.durationSec - voice);
|
|
438
|
+
out.push(
|
|
439
|
+
gap > allowed
|
|
440
|
+
? fail(
|
|
441
|
+
"frames.duration",
|
|
442
|
+
"after",
|
|
443
|
+
"quality",
|
|
444
|
+
`The video is ${probe.durationSec} s and the voice is ${Math.round(voice * 10) / 10} s; they may differ by ${Math.round(allowed * 10) / 10} s.`,
|
|
445
|
+
"make the picture last as long as the voice: end the scene at the last line's end time.",
|
|
446
|
+
attempt,
|
|
447
|
+
)
|
|
448
|
+
: pass("frames.duration", "after", "quality", `${probe.durationSec} s against a ${Math.round(voice * 10) / 10} s voice.`, attempt),
|
|
449
|
+
);
|
|
450
|
+
} else {
|
|
451
|
+
out.push(
|
|
452
|
+
probe.durationSec < 1
|
|
453
|
+
? fail("frames.duration", "after", "quality", `The video is ${probe.durationSec} s long.`, "render again; a short has at least a few seconds.", attempt)
|
|
454
|
+
: pass("frames.duration", "after", "quality", `${probe.durationSec} s (no voice to compare with).`, attempt),
|
|
455
|
+
);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Without a voice the picture's length is the template's, not a reading speed, so there is no pace to measure.
|
|
459
|
+
if (input.words && input.words > 0 && voice) {
|
|
460
|
+
const basis = voice;
|
|
461
|
+
const wps = input.words / basis;
|
|
462
|
+
const rounded = Math.round(wps * 10) / 10;
|
|
463
|
+
out.push(
|
|
464
|
+
wps < THRESHOLDS.paceMinWps || wps > THRESHOLDS.paceMaxWps
|
|
465
|
+
? fail(
|
|
466
|
+
"pace.measured",
|
|
467
|
+
"after",
|
|
468
|
+
"quality",
|
|
469
|
+
`${input.words} words over ${Math.round(basis * 10) / 10} s is ${rounded} words per second; a viewer can follow ${THRESHOLDS.paceMinWps} to ${THRESHOLDS.paceMaxWps}.`,
|
|
470
|
+
wps > THRESHOLDS.paceMaxWps ? "cut words or lengthen the picture; the voice races." : "add words or shorten the picture; the voice drags.",
|
|
471
|
+
attempt,
|
|
472
|
+
)
|
|
473
|
+
: pass("pace.measured", "after", "quality", `${rounded} words per second, inside ${THRESHOLDS.paceMinWps} to ${THRESHOLDS.paceMaxWps}.`, attempt),
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
out.push(
|
|
478
|
+
probe.width !== THRESHOLDS.width || probe.height !== THRESHOLDS.height
|
|
479
|
+
? fail(
|
|
480
|
+
"frames.resolution",
|
|
481
|
+
"after",
|
|
482
|
+
"quality",
|
|
483
|
+
`The video is ${probe.width}×${probe.height}; a short is ${THRESHOLDS.width}×${THRESHOLDS.height}.`,
|
|
484
|
+
`make the scene root ${THRESHOLDS.width}×${THRESHOLDS.height}.`,
|
|
485
|
+
attempt,
|
|
486
|
+
)
|
|
487
|
+
: pass("frames.resolution", "after", "quality", `${THRESHOLDS.width}×${THRESHOLDS.height}.`, attempt),
|
|
488
|
+
);
|
|
489
|
+
|
|
490
|
+
const motion = await tools.sampleMotion(input.file, probe.durationSec);
|
|
491
|
+
if (!motion) {
|
|
492
|
+
out.push(skipped("frames.black", "after", "quality", "The frames could not be sampled.", attempt));
|
|
493
|
+
out.push(skipped("frames.frozen", "after", "quality", "The frames could not be sampled.", attempt));
|
|
494
|
+
} else {
|
|
495
|
+
const black = motion.blackSec / probe.durationSec;
|
|
496
|
+
const frozen = motion.frozenSec / probe.durationSec;
|
|
497
|
+
const pct = (value: number) => `${Math.round(value * 100)}%`;
|
|
498
|
+
out.push(
|
|
499
|
+
black > THRESHOLDS.blackShare
|
|
500
|
+
? fail("frames.black", "after", "quality", `${pct(black)} of the video is black.`, "the scene drew nothing: check that its background and text are painted at the first frame.", attempt)
|
|
501
|
+
: pass("frames.black", "after", "quality", `${pct(black)} black, under ${pct(THRESHOLDS.blackShare)}.`, attempt),
|
|
502
|
+
);
|
|
503
|
+
out.push(
|
|
504
|
+
frozen > THRESHOLDS.frozenShare
|
|
505
|
+
? fail("frames.frozen", "after", "quality", `${pct(frozen)} of the video does not change.`, "the scene's animation did not run: drive it from the timeline, not from timers or requestAnimationFrame.", attempt)
|
|
506
|
+
: pass("frames.frozen", "after", "quality", `${pct(frozen)} frozen, under ${pct(THRESHOLDS.frozenShare)}.`, attempt),
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (!input.labelBurned) {
|
|
511
|
+
out.push(pass("label.present", "after", "compliance", "The AI-GENERATED label is drawn by the built-in template.", attempt));
|
|
512
|
+
} else {
|
|
513
|
+
const match = await tools.labelMatch(input.file, probe.durationSec / 2);
|
|
514
|
+
out.push(
|
|
515
|
+
match === null
|
|
516
|
+
? skipped("label.present", "after", "compliance", "The label position could not be read.", attempt)
|
|
517
|
+
: match < THRESHOLDS.labelMatchShare
|
|
518
|
+
? fail(
|
|
519
|
+
"label.present",
|
|
520
|
+
"after",
|
|
521
|
+
"compliance",
|
|
522
|
+
`The AI-GENERATED label was not found in the finished video (${Math.round(match * 100)}% of its border, needs ${Math.round(THRESHOLDS.labelMatchShare * 100)}%).`,
|
|
523
|
+
"do not publish this file; render again so vfrey burns the label, and do not draw over the bottom right corner.",
|
|
524
|
+
attempt,
|
|
525
|
+
)
|
|
526
|
+
: pass("label.present", "after", "compliance", "The AI-GENERATED label is on the finished video.", attempt),
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ------------------------------------------------------------------ real tools
|
|
533
|
+
|
|
534
|
+
const SKY: [number, number, number] = [0x90, 0xca, 0xf9];
|
|
535
|
+
|
|
536
|
+
/** Share of the label's border pixels that are sky-blue in a raw rgb24 crop of the label rectangle. Pure. */
|
|
537
|
+
export function labelBorderMatch(rgb: Buffer, width: number, height: number, border = 3): number {
|
|
538
|
+
let border_ = 0;
|
|
539
|
+
let found = 0;
|
|
540
|
+
for (let y = 0; y < height; y += 1) {
|
|
541
|
+
for (let x = 0; x < width; x += 1) {
|
|
542
|
+
if (!(x < border || y < border || x >= width - border || y >= height - border)) continue;
|
|
543
|
+
border_ += 1;
|
|
544
|
+
const at = (y * width + x) * 3;
|
|
545
|
+
if (Math.abs(rgb[at]! - SKY[0]) < 40 && Math.abs(rgb[at + 1]! - SKY[1]) < 40 && Math.abs(rgb[at + 2]! - SKY[2]) < 40) found += 1;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return border_ === 0 ? 0 : found / border_;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
/** Adds up the lengths ffmpeg prints for black and frozen stretches. Pure. */
|
|
552
|
+
export function parseMotionLog(log: string): { blackSec: number; frozenSec: number } {
|
|
553
|
+
let blackSec = 0;
|
|
554
|
+
let frozenSec = 0;
|
|
555
|
+
for (const match of log.matchAll(/black_duration:([\d.]+)/g)) blackSec += Number(match[1]);
|
|
556
|
+
for (const match of log.matchAll(/freeze_duration:\s*([\d.]+)/g)) frozenSec += Number(match[1]);
|
|
557
|
+
return { blackSec, frozenSec };
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
type Env = Record<string, string | undefined>;
|
|
561
|
+
|
|
562
|
+
export function realTools(env: Env = process.env): ReviewTools {
|
|
563
|
+
const exec = promisify(execFile);
|
|
564
|
+
const opts = { env: env as NodeJS.ProcessEnv, timeout: 5 * 60_000, maxBuffer: 32 * 1024 * 1024 };
|
|
565
|
+
return {
|
|
566
|
+
async available() {
|
|
567
|
+
try {
|
|
568
|
+
await exec("ffprobe", ["-version"], { env: env as NodeJS.ProcessEnv, timeout: 15_000 });
|
|
569
|
+
await exec("ffmpeg", ["-version"], { env: env as NodeJS.ProcessEnv, timeout: 15_000 });
|
|
570
|
+
return true;
|
|
571
|
+
} catch {
|
|
572
|
+
return false;
|
|
573
|
+
}
|
|
574
|
+
},
|
|
575
|
+
async probe(file) {
|
|
576
|
+
const { probeVideo } = await import("../render/probe.js");
|
|
577
|
+
return probeVideo(file, env);
|
|
578
|
+
},
|
|
579
|
+
async voiceSeconds(file) {
|
|
580
|
+
const { voiceSeconds } = await import("../render/html-engine.js");
|
|
581
|
+
return voiceSeconds(file, env);
|
|
582
|
+
},
|
|
583
|
+
async sampleMotion(file) {
|
|
584
|
+
try {
|
|
585
|
+
// Two frames a second are enough to see black or a frozen picture, and cost a fraction of a full decode.
|
|
586
|
+
const { stderr } = await exec(
|
|
587
|
+
"ffmpeg",
|
|
588
|
+
["-hide_banner", "-nostats", "-i", file, "-an", "-vf", "fps=2,blackdetect=d=1:pic_th=0.98:pix_th=0.10,freezedetect=n=0.001:d=3", "-f", "null", "-"],
|
|
589
|
+
opts,
|
|
590
|
+
);
|
|
591
|
+
return parseMotionLog(String(stderr));
|
|
592
|
+
} catch {
|
|
593
|
+
return null;
|
|
594
|
+
}
|
|
595
|
+
},
|
|
596
|
+
async labelMatch(file, atSec) {
|
|
597
|
+
try {
|
|
598
|
+
const { width, height } = labelSize();
|
|
599
|
+
const { x, y } = labelPosition();
|
|
600
|
+
const { stdout } = await exec(
|
|
601
|
+
"ffmpeg",
|
|
602
|
+
["-v", "error", "-ss", String(Math.max(0, atSec)), "-i", file, "-frames:v", "1", "-vf", `crop=${width}:${height}:${x}:${y}`, "-f", "rawvideo", "-pix_fmt", "rgb24", "-"],
|
|
603
|
+
{ ...opts, encoding: "buffer" },
|
|
604
|
+
);
|
|
605
|
+
const buf = stdout as unknown as Buffer;
|
|
606
|
+
return buf.length >= width * height * 3 ? labelBorderMatch(buf, width, height) : null;
|
|
607
|
+
} catch {
|
|
608
|
+
return null;
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// ------------------------------------------------------------------ outcome
|
|
615
|
+
|
|
616
|
+
export type GateOutcome =
|
|
617
|
+
| { kind: "pass" }
|
|
618
|
+
/** Compliance. The job is `blocked`; nothing retries. */
|
|
619
|
+
| { kind: "blocked"; failed: CheckResult[] }
|
|
620
|
+
/** Quality. The job is `rejected`. `retry` is true when the failed checks ran on a rendered file that a re-render can change. */
|
|
621
|
+
| { kind: "rejected"; failed: CheckResult[]; retry: boolean };
|
|
622
|
+
|
|
623
|
+
export function gateOutcome(results: CheckResult[]): GateOutcome {
|
|
624
|
+
const failed = results.filter((item) => item.status === "fail");
|
|
625
|
+
if (failed.length === 0) return { kind: "pass" };
|
|
626
|
+
const compliance = failed.filter((item) => item.severity === "compliance");
|
|
627
|
+
if (compliance.length > 0) return { kind: "blocked", failed: compliance };
|
|
628
|
+
return { kind: "rejected", failed, retry: failed.every((item) => item.phase === "after") };
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/** One line for an error or the tick summary: what failed, with the first Fix after the marker the CLI splits on. */
|
|
632
|
+
export function failureMessage(failed: CheckResult[]): { code: string; message: string } {
|
|
633
|
+
const first = failed[0]!;
|
|
634
|
+
const more = failed.length > 1 ? ` (and ${failed.length - 1} more: ${failed.slice(1).map((item) => item.id).join(", ")})` : "";
|
|
635
|
+
return { code: first.id, message: `${first.message}${more}${first.fix ? ` Fix: ${first.fix}` : ""}` };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/** Replaces the results of the phases just reviewed and keeps the others, so a payload always holds the latest of each check. */
|
|
639
|
+
export function mergeChecks(previous: CheckResult[] | undefined, fresh: CheckResult[]): CheckResult[] {
|
|
640
|
+
const ids = new Set(fresh.map((item) => item.id));
|
|
641
|
+
return [...(previous ?? []).filter((item) => !ids.has(item.id)), ...fresh];
|
|
642
|
+
}
|