@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,5 @@
|
|
|
1
|
+
The files in this folder (craft.md, motion.md, blocks.md) are original to vfrey in structure and wording, but their craft guidance is adapted from the skills and references of Framey, a modified version of HyperFrames by HeyGen (https://github.com/heygen-com/hyperframes), licensed under Apache-2.0.
|
|
2
|
+
|
|
3
|
+
Adapted and changed: the motion-craft defaults (ease by role, springs, overlap and stagger, holds, titles that land still, transitions with a cause, common faults), the caption placement and grouping advice, and the contrast and compression notes were rewritten for vfrey's 1080x1920 frame, its local GSAP, fonts and supplied helpers (vfrey.timeline, vfrey.at, vfrey.onWord, vfrey.onLine, vfrey.block), and its offline, deterministic renderer. Material about HyperFrames' own CLI, studio, registry, cloud and media tools was left out. The numbers were adjusted for vertical short video and the label that vfrey adds.
|
|
4
|
+
|
|
5
|
+
Copyright for the original work remains with HeyGen and the HyperFrames contributors. See src/scene/blocks/LICENSE (the Apache License, Version 2.0) and src/scene/blocks/NOTICE in the vfrey package. This is not affiliated with or endorsed by HeyGen.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Scene blocks
|
|
2
|
+
|
|
3
|
+
Blocks are ready-made pieces (lower thirds, charts, code panels, chat cards, overlays, backgrounds, transitions) you copy into your own scene and restyle. They are ingredients, not templates: the scene around them is still yours. HTML scenes only; a Remotion scene (`index.tsx`) gets a `Fix:` line, so write the piece in React instead.
|
|
4
|
+
|
|
5
|
+
## Find one
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
npx vfrey scene blocks # name, category, size, duration, fit
|
|
9
|
+
npx vfrey scene blocks --category chart # one category
|
|
10
|
+
npx vfrey scene blocks --json
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Categories: `lower-third`, `chart`, `diagram`, `chat`, `code`, `overlay`, `background`, `title`, `transition`. Choose by what the picture needs at that beat, and do not reach for a block when drawing the thing yourself says it better. At most one or two blocks in a short.
|
|
14
|
+
|
|
15
|
+
## Add and place it
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
npx vfrey scene add lt-clean-bar scenes/my-idea
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
This copies `blocks/<name>/<name>.html` into the scene folder and prints a snippet. Paste its `<div>` inside `#root`, after your own content so it paints on top (or before, for a background). Adding twice is a no-op; an edited copy is not overwritten without `--force`. Each block can be mounted **once** per scene (its composition id is its name).
|
|
22
|
+
|
|
23
|
+
The snippet's inline `style` already places it by the block's fit:
|
|
24
|
+
|
|
25
|
+
| Fit | Meaning |
|
|
26
|
+
| --- | --- |
|
|
27
|
+
| `native` | at most 1080 wide, centered as drawn |
|
|
28
|
+
| `crop` | the 1920x1080 canvas at full size, only its left 1080 px show: for pieces anchored left or at the bottom (lower thirds). Change `top` to move it; keep it clear of the caption zone and the label (see `craft.md`) |
|
|
29
|
+
| `scale` | the canvas is scaled to the frame width, a band across the middle; change `top` to move it |
|
|
30
|
+
| `cover` | scaled to the frame height and cropped at the sides: backgrounds and full-frame overlays |
|
|
31
|
+
|
|
32
|
+
Move or resize the mount freely; a block is ordinary HTML.
|
|
33
|
+
|
|
34
|
+
## Time it
|
|
35
|
+
|
|
36
|
+
A block is a sub-composition with its own GSAP timeline, seeked from `data-start`. Without a call it starts at 0. Set when it starts, after `const tl = vfrey.timeline()`:
|
|
37
|
+
|
|
38
|
+
```js
|
|
39
|
+
vfrey.onLine(1, (tl, t) => vfrey.block("#block-lt-clean-bar", t)); // when line 1 is spoken
|
|
40
|
+
vfrey.at(3.5, (tl, t) => vfrey.block("#block-data-chart", t, 4)); // at 3.5 s, for 4 s
|
|
41
|
+
vfrey.onWord(5, (tl, t) => vfrey.block("#block-snow", t)); // at a word
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`duration` is optional; each block plays for its own length otherwise. Do not start it after the film ends (voice length plus 0.3 s). If you need it again later, copy the folder under a different name or build a second instance yourself.
|
|
45
|
+
|
|
46
|
+
## Restyle it
|
|
47
|
+
|
|
48
|
+
Open `blocks/<name>/<name>.html`. Colors, fonts, sizes and text are plain CSS and markup:
|
|
49
|
+
|
|
50
|
+
- Replace the placeholder text ("Your Brand", "Track title", "@yourhandle") with words from the script. Never leave placeholder text or a real person's or company's name on screen.
|
|
51
|
+
- Swap its colors and fonts for your scene's variables (the local fonts are Titan One, JetBrains Mono, Google Sans Flex). A block that keeps its default look next to your palette is the most visible sign of a pasted-in piece.
|
|
52
|
+
- Change its timings with the `gsap` calls inside, but keep it self-contained: its own timeline, no network, no randomness, no clock.
|
|
53
|
+
- Do not add a CDN script, remote font or remote image; the render has no network.
|
|
54
|
+
|
|
55
|
+
## Checks
|
|
56
|
+
|
|
57
|
+
Render a still at the moment it shows (`--frame` a little after the cue) and look: is it where you want it, readable at phone size, clear of the captions and the label, not covering the focal element? Then a frame before the cue: it must not be visible yet.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# Scene craft
|
|
2
|
+
|
|
3
|
+
Read this before you design a scene. It is about the picture, not the commands (those are in `../SKILL.md`). Every number is for the 1080x1920 frame. These are defaults: the idea in front of you can break one on purpose, but know which.
|
|
4
|
+
|
|
5
|
+
## 1. Plan before you write HTML
|
|
6
|
+
|
|
7
|
+
1. Read the script aloud in your head and mark the **picture moments**: the hook, each claim, the turn, the last line. A short is 15 to 45 seconds; something on screen should change at every beat (`vfrey.timing.beats`), and nothing should sit unchanged for more than about 3 s.
|
|
8
|
+
2. Pick **one idea for the picture** that is not a text slide: a comparison drawn as two objects, a counter, a diagram that builds, a stick figure acting it out. Words are already in the voice and the captions; the picture must add something the words do not.
|
|
9
|
+
3. Write down the look as numbers before coding: ground color, ink color, one accent, one display font, one body font, a corner radius. Put them in CSS variables on `#root` and use only those.
|
|
10
|
+
4. Sketch the layout as three bands (below), then build it in that order: ground, big shapes, text, then motion.
|
|
11
|
+
|
|
12
|
+
## 2. The frame: bands and safe areas
|
|
13
|
+
|
|
14
|
+
```
|
|
15
|
+
y 0 ┌────────────────────────┐
|
|
16
|
+
│ top margin (0-160) │ keep clear: phone status bars, platform UI
|
|
17
|
+
160 ├────────────────────────┤
|
|
18
|
+
│ hero zone (160-1250) │ the picture and the headline
|
|
19
|
+
1250 ├────────────────────────┤
|
|
20
|
+
│ caption zone (1250-1700)│ captions live here by default
|
|
21
|
+
1700 ├────────────────────────┤
|
|
22
|
+
│ label zone (1700-1920) │ the AI-GENERATED label is at x 686-1008, y 1806-1864
|
|
23
|
+
y 1920 └────────────────────────┘
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
- Side margin 60 px minimum for anything that must be read; 80 px for text. Nothing important within 40 px of an edge.
|
|
27
|
+
- vfrey burns the `AI-GENERATED` label on top at the bottom right (about 322x58, right margin 72, bottom margin 56). Keep your own content out of x 650 to 1080, y 1780 to 1920. Never try to cover it.
|
|
28
|
+
- Captions are yours to place (`vfrey.captions("#captions", ...)`). The default box sits at `bottom: 220px`, about 250 px tall, which clears the label. If you move them up to make room for a bigger picture, keep the box at least 160 px above the label zone and never over the hero's focal point. Do not run a second block of text through the caption zone while captions are showing.
|
|
29
|
+
- Phones crop and overlay the lower right of a short (buttons, titles). Put the single most important thing in the upper two thirds, centered.
|
|
30
|
+
- Use full-width absolutely positioned containers with `text-align: center`. Avoid `left: 50%` plus `translateX(-50%)`: it clips at the frame edge once the element grows.
|
|
31
|
+
- Do not hard-code a pixel height for text you fill from the script; size by `max-width` and let it wrap, then check a frame.
|
|
32
|
+
|
|
33
|
+
## 3. Type
|
|
34
|
+
|
|
35
|
+
| Role | Size | Notes |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| Hero word or number | 220 to 420 px | one per beat, fills 60 to 90 % of the width |
|
|
38
|
+
| Headline | 90 to 130 px | at most 2 lines, 3 to 7 words |
|
|
39
|
+
| Support line / label | 48 to 64 px | never below 44 px: it is unreadable on a phone |
|
|
40
|
+
| Captions | 64 to 88 px | set through `vfrey.captions` options |
|
|
41
|
+
| Small chrome (units, tags) | 36 to 44 px | decoration only, never the message |
|
|
42
|
+
|
|
43
|
+
- Use the supplied fonts: Titan One (loud display, one weight), Google Sans Flex (clean body, many weights), JetBrains Mono (numbers, code, labels). Pair one display with one body; do not add a third. No Google Fonts link, the network is blocked.
|
|
44
|
+
- Line height 1.0 to 1.1 for display, 1.25 for body. Tight tracking (-0.01em) for big type, wide (0.08em) for small uppercase tags.
|
|
45
|
+
- Contrast ratio at least 4.5:1 against the ground right behind the text. White text on a mid-tone gets a dark scrim or a solid chip, not a drop shadow.
|
|
46
|
+
- Keep the same type scale for the whole film. Vary size by role, not at random.
|
|
47
|
+
- Highlight one word per line in the accent color. Two accents on one line fight.
|
|
48
|
+
|
|
49
|
+
## 4. Color
|
|
50
|
+
|
|
51
|
+
- A ground, an ink, one accent, and at most one support color. Choose them from the subject (its era, materials, the place it lives), not from a default palette. Pure white grounds with blue accents and dark purple gradients with neon are where unprompted scenes land; use them only when the idea is that.
|
|
52
|
+
- Full-screen linear gradients on dark grounds band after video compression. Use a solid fill, a radial gradient, or a solid with a local glow.
|
|
53
|
+
- An accent at 5 % opacity vanishes in compression: 15 to 25 % for atmosphere, full strength for the focal element.
|
|
54
|
+
- Change the ground color between acts of the story (hook, explanation, payoff) with a quick flood, so the film has chapters.
|
|
55
|
+
|
|
56
|
+
## 5. Motion principles
|
|
57
|
+
|
|
58
|
+
The timeline is `vfrey.timeline()`, one paused GSAP timeline. Cue with `vfrey.onLine(i, (tl, t) => ...)`, `vfrey.onWord(i, ...)` or `vfrey.at(sec, ...)`; every tween takes its position `t` so it lands on the spoken moment. Frames are a function of timeline time, so never use `Date.now`, `requestAnimationFrame`, CSS animations or `Math.random()` (use a seeded generator written inline if you need scatter).
|
|
59
|
+
|
|
60
|
+
- **Ease by role.** Arrivals: long tail (`power4.out`, `expo.out`). Camera moves and color floods: `power2.inOut`. Exits and zooms-through: `power3.in`. Linear only for rotation, progress and constant speed. The same `power2.out` at 0.5 s on everything is what makes a scene look generated.
|
|
61
|
+
- **Durations.** A fast move 0.3 to 0.6 s. Landing is slower than leaving; an exit is 40 to 50 % shorter than its entrance. Under 0.2 s reads as a glitch unless it is a deliberate whip.
|
|
62
|
+
- **Overlap.** Start the next element when the previous is 30 to 50 % done. Fully sequential looks robotic; everything at once looks cheap. Stagger: letters one frame apart, words 60 to 120 ms, list items 40 to 80 ms.
|
|
63
|
+
- **Springs.** `back.out(1.4)` or `elastic.out(1, 0.5)` for position and size only. Opacity and color never bounce. Bounce on UI reads as a toy; keep it for characters and objects.
|
|
64
|
+
- **Titles land dead still.** Once a title has arrived it stops. Move the next element, or the world behind it, instead. Do not let a headline creep with a slow scale, breathe, float or pulse; those are fillers.
|
|
65
|
+
- **Vary the entrance.** Not every element fades and slides up. Mix: per-word blur-in (`filter: blur(6px)` to 0, 0.13 s, 0.07 s apart), a mask or `clip-path` reveal, a wipe from a shape, a drop with a squash, a count-up, a line that draws itself (animate `strokeDashoffset`).
|
|
66
|
+
- **Motion that earns its place:** an object doing the thing the line says (the pieces assembling, the bar growing to its value, the figure walking), a slow push on a background (3 to 5 degrees or 2 to 4 % scale over several seconds), a diagram building step by step.
|
|
67
|
+
- **Hold to read.** A line of text the viewer must read holds about 0.5 s plus 0.3 s per word. The last line holds at least 1.5 s before the film ends (the film is voice length plus 0.3 s, so end your final tween before then).
|
|
68
|
+
- **Transitions have a cause**: a shape grows to become the next ground, the old content blurs out as the new blurs in on the same stage, a color floods across, the figure pushes the old panel off. Pick two or three kinds for a short, not a different one per beat. Do not crossfade between unrelated scenes.
|
|
69
|
+
- **Move with transforms.** Tween `x`, `y`, `scale`, `rotation`, `opacity`, `clipPath`, `filter`. Do not animate `left`, `top`, `width` or `height`; they relayout and jitter.
|
|
70
|
+
- **Never fight the timeline.** One tween per property per element at a time; use `fromTo` rather than `from` when an element is shown twice; a `tl.set(el, { opacity: 0 }, end)` hard-kills a hidden element so no stale frame stays; do not tween `visibility` with `.to` (use `set`).
|
|
71
|
+
|
|
72
|
+
## 6. Pacing to the voice
|
|
73
|
+
|
|
74
|
+
- `vfrey.timing.lines[i]` and `words[j]` carry `start` and `end` in seconds. Anchor each *picture change* to the start of its line (`onLine`) or to the key word (`onWord`), so the picture lands as the claim is spoken, not before and not after. Slight lead (0.05 to 0.15 s before the word) feels in sync.
|
|
75
|
+
- Number or object a line names should appear with the word that names it. A statistic appearing a beat after the voice says it looks like a mistake.
|
|
76
|
+
- Hook (line 0): something moving within the first 0.3 s, the hero element on screen by 1 s. No logo sting, no empty frame.
|
|
77
|
+
- Beats of one second or less: one element. Beats of three to six seconds: a small sequence, with motion inside the beat so it never sits still.
|
|
78
|
+
- Contrast speeds: a fast montage of three to five quick changes, then a held frame, then the payoff. Hold 0.3 to 0.7 s before the payoff.
|
|
79
|
+
- Layout changes on a bar-sized unit (a new line of the script), small accents on word beats.
|
|
80
|
+
|
|
81
|
+
## 7. Structure that survives a render
|
|
82
|
+
|
|
83
|
+
- One `#root` at 1080x1920, `data-composition-id="main"`, children absolutely positioned. Put every initial state in CSS (hidden things `opacity: 0`) so the first frame is right before the timeline runs.
|
|
84
|
+
- Build every animation synchronously, in the same script that calls `vfrey.timeline()`. No `await`, no `setTimeout`, no `fetch`: the timeline must be complete when the page loads.
|
|
85
|
+
- Draw art as inline SVG or SVG files in the scene folder (`<img src="octopus.svg">`) and the supplied `vfrey/assets/` stick figures and glyphs. Nothing from the network; no remote image, font or script.
|
|
86
|
+
- Prefix ids by area (`#hero-title`, `#cmp-left`) so selectors do not collide with a block's ids.
|
|
87
|
+
- When a scene is long, build it as functions per beat (`function beat3(t) { ... }`) called from `vfrey.onLine`, so each beat reads on its own.
|
|
88
|
+
|
|
89
|
+
## 8. Mistakes that make a scene look generated
|
|
90
|
+
|
|
91
|
+
- A text slide: the headline in the middle with a fade, then the next headline with the same fade.
|
|
92
|
+
- The same entrance on every element; everything fades and slides up 40 px.
|
|
93
|
+
- Captions over the picture's focal point, or a second text block in the caption zone.
|
|
94
|
+
- Content near the label (bottom right) or hugging an edge.
|
|
95
|
+
- Text under 44 px, or low contrast against a busy picture.
|
|
96
|
+
- Looping decoration (pulsing dots, floating shapes) used in place of meaning.
|
|
97
|
+
- Elements that never leave: the picture fills up with old content. Exit what is done, in one motion.
|
|
98
|
+
- A palette of five accent colors, or a default blue on white because no choice was made.
|
|
99
|
+
- Copying an example. Read one for the mechanics, then design for this idea.
|
|
100
|
+
- Reusing the previous video's scene with new text.
|
|
101
|
+
|
|
102
|
+
## 9. Check your own work
|
|
103
|
+
|
|
104
|
+
Render a still at the hook (`--frame 1`), a middle beat (`--frame` at a line start from `timing.lines`), and the last second, and look at each (`../SKILL.md` has the command). For each ask: is the focal element obvious in one glance? Is anything under the label or crossing the caption zone? Is any text under 44 px? Does the picture change at the beat? Fix, rerender, look again. Do not stop at one frame.
|
|
105
|
+
|
|
106
|
+
## 10. Change just this
|
|
107
|
+
|
|
108
|
+
When the person says "make the title red" or "the chart appears too late", do not rewrite the scene. Find the exact element, rule and tween, change those, and look again:
|
|
109
|
+
|
|
110
|
+
```
|
|
111
|
+
npx vfrey scene inspect my-scene --at 2.5 --text "the title words" # or --selector '#title'; add --json for everything
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
It loads the scene in the same headless Chrome as `render --frame` (network blocked), seeks the timeline to `--at`, and prints for each match: the selector path, the opening tag at `file:line:col`, its box in the 1080x1920 frame, computed color, background, font, opacity and transform, the CSS declarations behind them (`*` marks the one that wins; a value inherited from an ancestor says so) and the GSAP tweens that move it, each with start, duration, end, ease, properties, the line that wrote it and the `vfrey.at`, `onLine`, `onWord`, `block` or `transition` cue that ran it. Pick `--at` where the problem shows.
|
|
115
|
+
|
|
116
|
+
Then edit only what it names: "red" is that one declaration (or the winning one, if two rules fight, or the tween if GSAP sets `color`); "too late" is the tween's position argument or the `onLine`/`at` time of its cue; "too slow" is its duration. Do not touch other rules, tweens or elements. Then check with `render --frame` at the same `--at` (and `scene inspect` again if you moved timing). HTML scenes only: for a Remotion scene it prints a `Fix:` line; use `--frame` and edit `index.tsx`.
|
|
117
|
+
|
|
118
|
+
More: `motion.md` (recipes for GSAP moves, transitions, count-ups, draw-on lines), `blocks.md` (ready-made pieces such as lower thirds, charts, code and overlays to restyle).
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# Motion recipes
|
|
2
|
+
|
|
3
|
+
Copy the idea, not the numbers; change the ease, distance and timing for the idea at hand. All of this runs inside the single `const tl = vfrey.timeline()` and is cued with `vfrey.onLine`, `vfrey.onWord` or `vfrey.at`. Elements start hidden in CSS.
|
|
4
|
+
|
|
5
|
+
## Arrivals (choose a different one per beat)
|
|
6
|
+
|
|
7
|
+
```js
|
|
8
|
+
// Per-word blur-in (split the headline into spans yourself, no random)
|
|
9
|
+
const words = [...document.querySelectorAll("#head span")];
|
|
10
|
+
vfrey.onLine(0, (tl, t) =>
|
|
11
|
+
tl.fromTo(words, { opacity: 0, filter: "blur(6px)" },
|
|
12
|
+
{ opacity: 1, filter: "blur(0px)", duration: 0.16, ease: "power2.out", stagger: 0.08 }, t));
|
|
13
|
+
|
|
14
|
+
// Mask wipe from the left
|
|
15
|
+
vfrey.onLine(1, (tl, t) =>
|
|
16
|
+
tl.fromTo("#bar", { clipPath: "inset(0 100% 0 0)" }, { clipPath: "inset(0 0% 0 0)", duration: 0.5, ease: "power3.inOut" }, t));
|
|
17
|
+
|
|
18
|
+
// Drop with a squash (position and size may bounce)
|
|
19
|
+
vfrey.onLine(2, (tl, t) => {
|
|
20
|
+
tl.fromTo("#ball", { y: -300, scaleY: 1 }, { y: 0, duration: 0.45, ease: "power3.in" }, t)
|
|
21
|
+
.to("#ball", { scaleY: 0.78, transformOrigin: "50% 100%", duration: 0.08 }, t + 0.45)
|
|
22
|
+
.to("#ball", { scaleY: 1, duration: 0.3, ease: "elastic.out(1, 0.5)" }, t + 0.53);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Line that draws itself (SVG path)
|
|
26
|
+
const path = document.querySelector("#arrow path");
|
|
27
|
+
const len = path.getTotalLength();
|
|
28
|
+
gsap.set(path, { strokeDasharray: len, strokeDashoffset: len });
|
|
29
|
+
vfrey.onLine(3, (tl, t) => tl.to(path, { strokeDashoffset: 0, duration: 0.7, ease: "power2.inOut" }, t));
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Numbers and bars
|
|
33
|
+
|
|
34
|
+
```js
|
|
35
|
+
// Count-up: tween a plain object, write text in onUpdate (deterministic: no clock)
|
|
36
|
+
const n = { v: 0 };
|
|
37
|
+
vfrey.onWord(4, (tl, t) =>
|
|
38
|
+
tl.to(n, { v: 87, duration: 0.9, ease: "power3.out",
|
|
39
|
+
onUpdate: () => (document.getElementById("num").textContent = Math.round(n.v)) }, t));
|
|
40
|
+
|
|
41
|
+
// Bar to a value: scale a full-size bar, do not animate height
|
|
42
|
+
tl.fromTo("#fill", { scaleY: 0 }, { scaleY: 0.64, transformOrigin: "50% 100%", duration: 0.7, ease: "power4.out" }, t);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Only show a number that is in the script or that you are certain of. A chart is a picture of what the voice says, not new claims.
|
|
46
|
+
|
|
47
|
+
## Camera moves on a wrapper
|
|
48
|
+
|
|
49
|
+
Put the whole scene (or a stage) in `#world` and move that. A punch-in: `tl.to("#world", { scale: 1.12, x: -40, y: -60, duration: 0.5, ease: "power2.inOut" }, t)`. A hard cut inside a scene is a punch-in plus a content swap on the same frame, with matching direction on both sides. Transform-origin is set once, in CSS.
|
|
50
|
+
|
|
51
|
+
## Transitions
|
|
52
|
+
|
|
53
|
+
| Idea | How |
|
|
54
|
+
| --- | --- |
|
|
55
|
+
| Object becomes the world | a circle or card scales past the frame (0.6 s, `power2.inOut`); its fill is the next ground; the new content fades in while it still grows |
|
|
56
|
+
| Color flood | the hero stays; the ground changes color in about 0.4 s (`power2.inOut`) |
|
|
57
|
+
| Whip | a 0.15 to 0.3 s horizontal slide with a `blur(12px)` that sharpens on landing |
|
|
58
|
+
| Blur swap on one stage | out: opacity to 0 with `blur(8px)` in 0.15 s; in: blur to 0 in 0.15 s; the ground never changes |
|
|
59
|
+
| Iris | `clipPath: "circle(0% at 50% 60%)"` to `"circle(150% at 50% 60%)"` onto the next stage |
|
|
60
|
+
| Dip | darken the old frame by about 40 % for 3 frames (0.1 s) before a hard cut |
|
|
61
|
+
|
|
62
|
+
Mirror eases across a seam: `power3.in` going out, `power3.out` coming in.
|
|
63
|
+
|
|
64
|
+
### Shader transitions between shots (HTML scenes only)
|
|
65
|
+
|
|
66
|
+
GPU transitions (glitch, zoom blur, warp, iris, light leak and more) between full-frame shots; `npx vfrey scene transitions` lists them. Stack the shots as sibling `<div>`s in `#root` (each fills the frame), load `<script src="vfrey/transitions.js"></script>` before `vfrey/vfrey.js`, then cue each seam. Calls chain (shot A to B, then B to C), may not overlap, and are driven by the scene's timeline, so frames are the same on every render:
|
|
67
|
+
|
|
68
|
+
```js
|
|
69
|
+
vfrey.transition("#shot-a", "#shot-b", { type: "cross-warp-morph", at: 3.2, duration: 0.7 });
|
|
70
|
+
vfrey.transition("#shot-b", "#shot-c", { type: "glitch", at: 7.5, duration: 0.5 });
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Place `at` with `vfrey.line(i).start` to cut on a spoken line. Animate each shot's contents as usual on `tl` (both shots keep playing through the seam). Use a type per seam with a cause, about 0.4 to 0.8 s, never every beat; `crossfade` is the quiet one. A `--frame` of a scene with transitions renders the clip up to that frame, so it takes about as long as a short render. Remotion scenes cannot use them (`✗` with a Fix); use `interpolate()` there.
|
|
74
|
+
|
|
75
|
+
## Exits
|
|
76
|
+
|
|
77
|
+
One smooth motion, 40 to 50 % shorter than the entrance: blur-and-fade 0.13 to 0.25 s, or move with the scene's own transition. Finish with a hard kill so no stale frame lingers: `tl.set("#panel", { opacity: 0 }, end)`.
|
|
78
|
+
|
|
79
|
+
## Common faults
|
|
80
|
+
|
|
81
|
+
| Symptom | Usual cause |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| Blank first frame, then pop | initial state not in CSS; tween built after the page loaded |
|
|
84
|
+
| Jitter or drift between renders | layout measured in `onUpdate`; two tweens on the same property; `left/top` animated |
|
|
85
|
+
| Text clipped at the edge after it scales | `overflow: hidden` on a parent, or `translateX(-50%)` centering |
|
|
86
|
+
| Everything lands at once | every cue uses the same `t`; stagger or use different lines |
|
|
87
|
+
| A block shows too early | `vfrey.block` never called for it (it plays from `data-start` 0) |
|
|
88
|
+
| Nothing visible in the still at 2.5 s | the element's entrance is later than that; render a frame after its cue |
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
export interface DashboardRow {
|
|
2
|
+
accountId: string;
|
|
3
|
+
platform: string;
|
|
4
|
+
externalId: string;
|
|
5
|
+
views: number | null;
|
|
6
|
+
watchTimeSec: number | null;
|
|
7
|
+
retention: number | null;
|
|
8
|
+
estimatedRevenue: number | null;
|
|
9
|
+
revenueStatus: string;
|
|
10
|
+
capturedAt?: string | null;
|
|
11
|
+
rank: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface HealthRow {
|
|
15
|
+
accountId: string;
|
|
16
|
+
platform: string;
|
|
17
|
+
tokenState: string;
|
|
18
|
+
lastError: string | null;
|
|
19
|
+
postsLast7d: number;
|
|
20
|
+
targetPostsLast7d: number;
|
|
21
|
+
cadence: "on_track" | "behind" | "ahead";
|
|
22
|
+
strikes: "not_exposed_by_official_api";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function rankMetrics(
|
|
26
|
+
rows: Array<Omit<DashboardRow, "rank">>,
|
|
27
|
+
): DashboardRow[] {
|
|
28
|
+
const sorted = [...rows].sort((a, b) => {
|
|
29
|
+
const av = a.views ?? -1;
|
|
30
|
+
const bv = b.views ?? -1;
|
|
31
|
+
if (bv !== av) return bv - av;
|
|
32
|
+
return (b.watchTimeSec ?? -1) - (a.watchTimeSec ?? -1);
|
|
33
|
+
});
|
|
34
|
+
return sorted.map((row, index) => ({ ...row, rank: index + 1 }));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function cadenceHealth(postsLast7d: number, target: number): HealthRow["cadence"] {
|
|
38
|
+
if (postsLast7d + 0.5 < target * 0.6) return "behind";
|
|
39
|
+
if (postsLast7d > target * 1.4) return "ahead";
|
|
40
|
+
return "on_track";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function renderDashboard(input: { rows: DashboardRow[]; health: HealthRow[]; generatedAt: string }): string {
|
|
44
|
+
const body = input.rows
|
|
45
|
+
.map(
|
|
46
|
+
(row) => `<tr>
|
|
47
|
+
<td>${row.rank}</td>
|
|
48
|
+
<td>${esc(row.accountId)}</td>
|
|
49
|
+
<td>${esc(row.platform)}</td>
|
|
50
|
+
<td>${esc(row.externalId)}</td>
|
|
51
|
+
<td>${cell(row.views)}</td>
|
|
52
|
+
<td>${cell(row.watchTimeSec)}</td>
|
|
53
|
+
<td>${row.retention === null ? "—" : `${cell(row.retention)}%`}</td>
|
|
54
|
+
<td>${row.revenueStatus === "present" ? cell(row.estimatedRevenue) : `— (${esc(row.revenueStatus)}: not available from the official API)`}</td>
|
|
55
|
+
<td>${row.capturedAt ? esc(row.capturedAt) : "—"}</td>
|
|
56
|
+
</tr>`,
|
|
57
|
+
)
|
|
58
|
+
.join("\n");
|
|
59
|
+
const health = input.health
|
|
60
|
+
.map(
|
|
61
|
+
(row) => `<tr>
|
|
62
|
+
<td>${esc(row.accountId)}</td>
|
|
63
|
+
<td>${esc(row.platform)}</td>
|
|
64
|
+
<td>${esc(row.tokenState)}</td>
|
|
65
|
+
<td>${esc(row.cadence)}</td>
|
|
66
|
+
<td>${row.postsLast7d}/${row.targetPostsLast7d}</td>
|
|
67
|
+
<td>${esc(row.strikes)}</td>
|
|
68
|
+
<td>${esc(row.lastError ?? "")}</td>
|
|
69
|
+
</tr>`,
|
|
70
|
+
)
|
|
71
|
+
.join("\n");
|
|
72
|
+
return `<!doctype html>
|
|
73
|
+
<html lang="en">
|
|
74
|
+
<meta charset="utf-8">
|
|
75
|
+
<title>Pipeline analytics</title>
|
|
76
|
+
<style>
|
|
77
|
+
body { font: 15px/1.4 "Iowan Old Style", Palatino, serif; background: #f6f3ec; color: #1b1914; margin: 32px; }
|
|
78
|
+
h1 { font-weight: 500; letter-spacing: -0.03em; }
|
|
79
|
+
table { border-collapse: collapse; width: 100%; background: white; margin: 12px 0 28px; }
|
|
80
|
+
th, td { border-bottom: 1px solid #e4ddd0; text-align: left; padding: 8px 10px; font-variant-numeric: tabular-nums; }
|
|
81
|
+
th { font: 12px/1.2 ui-sans-serif, sans-serif; letter-spacing: 0.06em; text-transform: uppercase; color: #665e52; }
|
|
82
|
+
p { color: #665e52; }
|
|
83
|
+
</style>
|
|
84
|
+
<h1>Account ranking</h1>
|
|
85
|
+
<p>Generated ${esc(input.generatedAt)}. Watch time, retention, and revenue stay blank when the official API does not return them. A forbidden revenue scope is not shown as zero. Strike status is not exposed by these APIs.</p>
|
|
86
|
+
<table>
|
|
87
|
+
<thead><tr><th>Rank</th><th>Account</th><th>Platform</th><th>Post</th><th>Views</th><th>Watch sec</th><th>Retention</th><th>Revenue</th><th>As of</th></tr></thead>
|
|
88
|
+
<tbody>${body || `<tr><td colspan="9">No snapshots yet.</td></tr>`}</tbody>
|
|
89
|
+
</table>
|
|
90
|
+
<h1>Account health</h1>
|
|
91
|
+
<table>
|
|
92
|
+
<thead><tr><th>Account</th><th>Platform</th><th>Token</th><th>Cadence</th><th>Posts 7d</th><th>Strikes</th><th>Last error</th></tr></thead>
|
|
93
|
+
<tbody>${health || `<tr><td colspan="7">No health rows yet.</td></tr>`}</tbody>
|
|
94
|
+
</table>
|
|
95
|
+
</html>`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function cell(value: number | null): string {
|
|
99
|
+
if (value === null || Number.isNaN(value)) return "—";
|
|
100
|
+
return esc(String(value));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function esc(value: string): string {
|
|
104
|
+
return value
|
|
105
|
+
.replace(/&/g, "&")
|
|
106
|
+
.replace(/</g, "<")
|
|
107
|
+
.replace(/>/g, ">")
|
|
108
|
+
.replace(/"/g, """);
|
|
109
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { interpretRevenue } from "@vfrey/contracts";
|
|
4
|
+
import type { Account, AppConfig } from "@vfrey/contracts";
|
|
5
|
+
import { requestJson } from "@vfrey/contracts";
|
|
6
|
+
import type { Store } from "../queue/store.js";
|
|
7
|
+
import { cadenceHealth, rankMetrics, renderDashboard, type HealthRow } from "./dashboard.js";
|
|
8
|
+
|
|
9
|
+
export async function captureAnalytics(input: {
|
|
10
|
+
config: AppConfig;
|
|
11
|
+
account: Account;
|
|
12
|
+
posts: Array<{ platform: string; externalId: string }>;
|
|
13
|
+
tokenFor: (platform: string) => Promise<string>;
|
|
14
|
+
store: Store;
|
|
15
|
+
fetchImpl: typeof fetch;
|
|
16
|
+
now: Date;
|
|
17
|
+
}): Promise<void> {
|
|
18
|
+
for (const post of input.posts) {
|
|
19
|
+
if (!post.externalId || post.externalId === "dry-run") continue;
|
|
20
|
+
const reading = await readPost(input, post.platform, post.externalId);
|
|
21
|
+
input.store.insertMetric({
|
|
22
|
+
accountId: input.account.id,
|
|
23
|
+
platform: post.platform,
|
|
24
|
+
externalId: post.externalId,
|
|
25
|
+
capturedAt: input.now.toISOString(),
|
|
26
|
+
views: reading.views,
|
|
27
|
+
watchTimeSec: reading.watchTimeSec,
|
|
28
|
+
retention: reading.retention,
|
|
29
|
+
estimatedRevenue: reading.estimatedRevenue,
|
|
30
|
+
revenueStatus: reading.revenueStatus,
|
|
31
|
+
raw: reading.raw,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface Reading {
|
|
37
|
+
views: number | null;
|
|
38
|
+
watchTimeSec: number | null;
|
|
39
|
+
retention: number | null;
|
|
40
|
+
estimatedRevenue: number | null;
|
|
41
|
+
revenueStatus: string;
|
|
42
|
+
raw: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function readPost(
|
|
46
|
+
input: { config: AppConfig; tokenFor: (platform: string) => Promise<string>; fetchImpl: typeof fetch },
|
|
47
|
+
platform: string,
|
|
48
|
+
externalId: string,
|
|
49
|
+
): Promise<Reading> {
|
|
50
|
+
if (platform === "youtube") return readYouTube(input, externalId);
|
|
51
|
+
if (platform === "tiktok") return readTikTok(input, externalId);
|
|
52
|
+
if (platform === "instagram") return readInstagram(input, externalId);
|
|
53
|
+
if (platform === "facebook") return readFacebook(input, externalId);
|
|
54
|
+
return empty("unavailable");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function readYouTube(
|
|
58
|
+
input: { tokenFor: (platform: string) => Promise<string>; fetchImpl: typeof fetch },
|
|
59
|
+
videoId: string,
|
|
60
|
+
): Promise<Reading> {
|
|
61
|
+
const token = await input.tokenFor("youtube");
|
|
62
|
+
const stats = await requestJson(
|
|
63
|
+
input.fetchImpl,
|
|
64
|
+
`https://www.googleapis.com/youtube/v3/videos?part=statistics&id=${encodeURIComponent(videoId)}`,
|
|
65
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
66
|
+
);
|
|
67
|
+
const viewsRaw = (stats.json as { items?: Array<{ statistics?: { viewCount?: string } }> }).items?.[0]?.statistics?.viewCount;
|
|
68
|
+
const views = viewsRaw === undefined ? null : Number(viewsRaw);
|
|
69
|
+
const end = new Date().toISOString().slice(0, 10);
|
|
70
|
+
const startDate = new Date(Date.now() - 28 * 24 * 60 * 60 * 1000).toISOString().slice(0, 10);
|
|
71
|
+
const report = await requestJson(
|
|
72
|
+
input.fetchImpl,
|
|
73
|
+
"https://youtubeanalytics.googleapis.com/v2/reports?" +
|
|
74
|
+
new URLSearchParams({
|
|
75
|
+
ids: "channel==MINE",
|
|
76
|
+
startDate,
|
|
77
|
+
endDate: end,
|
|
78
|
+
metrics: "estimatedMinutesWatched,averageViewPercentage",
|
|
79
|
+
filters: `video==${videoId}`,
|
|
80
|
+
}).toString(),
|
|
81
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
82
|
+
);
|
|
83
|
+
let watchTimeSec: number | null = null;
|
|
84
|
+
let retention: number | null = null;
|
|
85
|
+
if (report.status < 400) {
|
|
86
|
+
const row = (report.json as { rows?: number[][] }).rows?.[0];
|
|
87
|
+
if (row) {
|
|
88
|
+
watchTimeSec = typeof row[0] === "number" ? row[0] * 60 : null;
|
|
89
|
+
retention = typeof row[1] === "number" ? row[1] : null;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
const money = await requestJson(
|
|
93
|
+
input.fetchImpl,
|
|
94
|
+
"https://youtubeanalytics.googleapis.com/v2/reports?" +
|
|
95
|
+
new URLSearchParams({
|
|
96
|
+
ids: "channel==MINE",
|
|
97
|
+
startDate,
|
|
98
|
+
endDate: end,
|
|
99
|
+
metrics: "estimatedRevenue",
|
|
100
|
+
filters: `video==${videoId}`,
|
|
101
|
+
}).toString(),
|
|
102
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
103
|
+
);
|
|
104
|
+
const revenueValue =
|
|
105
|
+
money.status < 400 ? ((money.json as { rows?: number[][] }).rows?.[0]?.[0] ?? null) : null;
|
|
106
|
+
const revenue = interpretRevenue({
|
|
107
|
+
value: typeof revenueValue === "number" ? revenueValue : null,
|
|
108
|
+
httpStatus: money.status,
|
|
109
|
+
});
|
|
110
|
+
return {
|
|
111
|
+
views: Number.isFinite(views) ? views : null,
|
|
112
|
+
watchTimeSec,
|
|
113
|
+
retention,
|
|
114
|
+
estimatedRevenue: revenue.estimatedRevenue,
|
|
115
|
+
revenueStatus: revenue.revenueStatus,
|
|
116
|
+
raw: { stats: stats.status, analytics: report.status, revenue: money.status },
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function readTikTok(
|
|
121
|
+
input: { tokenFor: (platform: string) => Promise<string>; fetchImpl: typeof fetch },
|
|
122
|
+
externalId: string,
|
|
123
|
+
): Promise<Reading> {
|
|
124
|
+
const token = await input.tokenFor("tiktok");
|
|
125
|
+
const result = await requestJson(
|
|
126
|
+
input.fetchImpl,
|
|
127
|
+
"https://open.tiktokapis.com/v2/video/query/?fields=id,view_count,like_count,comment_count,share_count",
|
|
128
|
+
{
|
|
129
|
+
method: "POST",
|
|
130
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
131
|
+
body: JSON.stringify({ filters: { video_ids: [externalId] } }),
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
const video = (result.json as { data?: { videos?: Array<{ view_count?: number }> } }).data?.videos?.[0];
|
|
135
|
+
return {
|
|
136
|
+
views: typeof video?.view_count === "number" ? video.view_count : null,
|
|
137
|
+
watchTimeSec: null,
|
|
138
|
+
retention: null,
|
|
139
|
+
estimatedRevenue: null,
|
|
140
|
+
revenueStatus: "unavailable",
|
|
141
|
+
raw: { status: result.status, note: "Display API does not return watch time or revenue" },
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function readInstagram(
|
|
146
|
+
input: { config: AppConfig; tokenFor: (platform: string) => Promise<string>; fetchImpl: typeof fetch },
|
|
147
|
+
mediaId: string,
|
|
148
|
+
): Promise<Reading> {
|
|
149
|
+
const token = await input.tokenFor("instagram");
|
|
150
|
+
const version = input.config.meta.graphVersion;
|
|
151
|
+
const result = await requestJson(
|
|
152
|
+
input.fetchImpl,
|
|
153
|
+
`https://graph.facebook.com/${version}/${mediaId}/insights?metric=views`,
|
|
154
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
155
|
+
);
|
|
156
|
+
const value = (result.json as { data?: Array<{ values?: Array<{ value?: number }> }> }).data?.[0]?.values?.[0]?.value;
|
|
157
|
+
return {
|
|
158
|
+
views: typeof value === "number" ? value : null,
|
|
159
|
+
watchTimeSec: null,
|
|
160
|
+
retention: null,
|
|
161
|
+
estimatedRevenue: null,
|
|
162
|
+
revenueStatus: "unavailable",
|
|
163
|
+
raw: { status: result.status, note: "Organic Instagram insights do not include estimated revenue" },
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
async function readFacebook(
|
|
168
|
+
input: { config: AppConfig; tokenFor: (platform: string) => Promise<string>; fetchImpl: typeof fetch },
|
|
169
|
+
videoId: string,
|
|
170
|
+
): Promise<Reading> {
|
|
171
|
+
const token = await input.tokenFor("facebook");
|
|
172
|
+
const version = input.config.meta.graphVersion;
|
|
173
|
+
const result = await requestJson(
|
|
174
|
+
input.fetchImpl,
|
|
175
|
+
`https://graph.facebook.com/${version}/${videoId}/video_insights?metric=total_video_views,total_video_view_total_time`,
|
|
176
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
177
|
+
);
|
|
178
|
+
const data = (result.json as { data?: Array<{ name?: string; values?: Array<{ value?: number }> }> }).data ?? [];
|
|
179
|
+
const views = data.find((item) => item.name === "total_video_views")?.values?.[0]?.value;
|
|
180
|
+
const watch = data.find((item) => item.name === "total_video_view_total_time")?.values?.[0]?.value;
|
|
181
|
+
return {
|
|
182
|
+
views: typeof views === "number" ? views : null,
|
|
183
|
+
watchTimeSec: typeof watch === "number" ? watch / 1000 : null,
|
|
184
|
+
retention: null,
|
|
185
|
+
estimatedRevenue: null,
|
|
186
|
+
revenueStatus: "unavailable",
|
|
187
|
+
raw: { status: result.status },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function empty(revenueStatus: string): Reading {
|
|
192
|
+
return {
|
|
193
|
+
views: null,
|
|
194
|
+
watchTimeSec: null,
|
|
195
|
+
retention: null,
|
|
196
|
+
estimatedRevenue: null,
|
|
197
|
+
revenueStatus,
|
|
198
|
+
raw: {},
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export async function writeDashboard(input: {
|
|
203
|
+
store: Store;
|
|
204
|
+
accounts: Account[];
|
|
205
|
+
dataDir: string;
|
|
206
|
+
now: Date;
|
|
207
|
+
}): Promise<string> {
|
|
208
|
+
const metrics = input.store.latestMetrics().map((row) => ({
|
|
209
|
+
accountId: String(row.account_id),
|
|
210
|
+
platform: String(row.platform),
|
|
211
|
+
externalId: String(row.external_id),
|
|
212
|
+
views: row.views === null || row.views === undefined ? null : Number(row.views),
|
|
213
|
+
watchTimeSec: row.watch_time_sec === null || row.watch_time_sec === undefined ? null : Number(row.watch_time_sec),
|
|
214
|
+
retention: row.retention === null || row.retention === undefined ? null : Number(row.retention),
|
|
215
|
+
estimatedRevenue:
|
|
216
|
+
row.estimated_revenue === null || row.estimated_revenue === undefined ? null : Number(row.estimated_revenue),
|
|
217
|
+
revenueStatus: String(row.revenue_status),
|
|
218
|
+
capturedAt: row.captured_at ? String(row.captured_at) : null,
|
|
219
|
+
}));
|
|
220
|
+
const ranked = rankMetrics(metrics);
|
|
221
|
+
const healthDb = new Map(
|
|
222
|
+
input.store.healthRows().map((row) => [`${row.account_id}:${row.platform}`, row]),
|
|
223
|
+
);
|
|
224
|
+
const health: HealthRow[] = [];
|
|
225
|
+
for (const account of input.accounts) {
|
|
226
|
+
const times = input.store.publicationTimes(account.id, 100);
|
|
227
|
+
const weekAgo = input.now.getTime() - 7 * 24 * 60 * 60 * 1000;
|
|
228
|
+
const postsLast7d = times.filter((time) => time.getTime() >= weekAgo).length;
|
|
229
|
+
const overlay = input.store.deskOverlay(account.id);
|
|
230
|
+
const maxPosts = overlay ? overlay.maxPostsPerDay : account.cadence.maxPostsPerDay;
|
|
231
|
+
const target = maxPosts * 7;
|
|
232
|
+
for (const platform of Object.keys(account.platforms)) {
|
|
233
|
+
const row = healthDb.get(`${account.id}:${platform}`);
|
|
234
|
+
health.push({
|
|
235
|
+
accountId: account.id,
|
|
236
|
+
platform,
|
|
237
|
+
tokenState: row ? String(row.token_state) : "unknown",
|
|
238
|
+
lastError: row?.last_error ? String(row.last_error) : null,
|
|
239
|
+
postsLast7d,
|
|
240
|
+
targetPostsLast7d: target,
|
|
241
|
+
cadence: cadenceHealth(postsLast7d, target),
|
|
242
|
+
strikes: "not_exposed_by_official_api",
|
|
243
|
+
});
|
|
244
|
+
const label = cadenceHealth(postsLast7d, target);
|
|
245
|
+
if (label === "behind" || label === "ahead") {
|
|
246
|
+
input.store.warnOnce({
|
|
247
|
+
accountId: account.id,
|
|
248
|
+
jobId: null,
|
|
249
|
+
code: `cadence_${label}`,
|
|
250
|
+
message: `${account.id} is ${label}.`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
const html = renderDashboard({ rows: ranked, health, generatedAt: input.now.toISOString() });
|
|
256
|
+
const file = path.join(input.dataDir, "dashboard.html");
|
|
257
|
+
await mkdir(input.dataDir, { recursive: true });
|
|
258
|
+
await writeFile(file, html);
|
|
259
|
+
return file;
|
|
260
|
+
}
|