@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,217 @@
|
|
|
1
|
+
import { ConfigError, RetryableError } from "@vfrey/contracts";
|
|
2
|
+
import { classifyComment, reviewReply, type ReplyTone } from "@vfrey/contracts";
|
|
3
|
+
import type { Account, AppConfig } from "@vfrey/contracts";
|
|
4
|
+
import { requestJson } from "@vfrey/contracts";
|
|
5
|
+
import type { LlmClient } from "../llm/client.js";
|
|
6
|
+
import type { Store } from "../queue/store.js";
|
|
7
|
+
|
|
8
|
+
export interface OwnedPost {
|
|
9
|
+
platform: "youtube" | "instagram" | "facebook";
|
|
10
|
+
externalId: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function engageAccount(input: {
|
|
14
|
+
config: AppConfig;
|
|
15
|
+
account: Account;
|
|
16
|
+
posts: OwnedPost[];
|
|
17
|
+
tokenFor: (platform: OwnedPost["platform"]) => Promise<string>;
|
|
18
|
+
llm: LlmClient;
|
|
19
|
+
store: Store;
|
|
20
|
+
fetchImpl: typeof fetch;
|
|
21
|
+
now: Date;
|
|
22
|
+
dryRun: boolean;
|
|
23
|
+
}): Promise<number> {
|
|
24
|
+
if (!input.account.engage.enabled) return 0;
|
|
25
|
+
let replied = 0;
|
|
26
|
+
const since = new Date(input.now.getTime() - 24 * 60 * 60 * 1000).toISOString();
|
|
27
|
+
for (const post of input.posts) {
|
|
28
|
+
const comments = await listComments(input, post);
|
|
29
|
+
for (const comment of comments) {
|
|
30
|
+
if (input.store.commentHandled(post.platform, comment.id)) continue;
|
|
31
|
+
const decision = classifyComment(comment.text, input.account.engage.allowSpicy);
|
|
32
|
+
if (!decision.reply) {
|
|
33
|
+
input.store.recordComment({
|
|
34
|
+
platform: post.platform,
|
|
35
|
+
commentId: comment.id,
|
|
36
|
+
accountId: input.account.id,
|
|
37
|
+
action: "skipped",
|
|
38
|
+
tone: null,
|
|
39
|
+
replyText: null,
|
|
40
|
+
at: input.now,
|
|
41
|
+
commentText: comment.text,
|
|
42
|
+
dryRun: input.dryRun,
|
|
43
|
+
});
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
if (input.store.repliesSince(input.account.id, since) >= input.account.engage.maxRepliesPerDay) return replied;
|
|
47
|
+
const last = input.store.lastReplyAt(input.account.id);
|
|
48
|
+
if (last && input.now.getTime() - last.getTime() < input.account.engage.minSecondsBetweenReplies * 1000) {
|
|
49
|
+
// End the pass. The reply gap is ours; the next tick continues from the comments not yet handled.
|
|
50
|
+
return replied;
|
|
51
|
+
}
|
|
52
|
+
const raw = await input.llm.complete({
|
|
53
|
+
system: [
|
|
54
|
+
"You reply to a comment on the creator's own video.",
|
|
55
|
+
"Match the requested tone. Spicy means a sharp one-beat pushback with no insult and no slur.",
|
|
56
|
+
"Never claim to be a doctor, lawyer, or financial adviser.",
|
|
57
|
+
"No links. Under 400 characters. Return JSON {tone, text}.",
|
|
58
|
+
].join(" "),
|
|
59
|
+
user: `TASK:REPLY\nTONE: ${decision.tone}\nCOMMENT: ${comment.text.replace(/\n/g, " ")}\nVIDEO: ${comment.videoTitle}`,
|
|
60
|
+
temperature: decision.tone === "spicy" ? 0.8 : 0.4,
|
|
61
|
+
maxTokens: 300,
|
|
62
|
+
});
|
|
63
|
+
const parsed = JSON.parse(extractJson(raw)) as { tone?: ReplyTone; text?: string };
|
|
64
|
+
const draft = { tone: decision.tone, text: String(parsed.text ?? "") };
|
|
65
|
+
const review = reviewReply(draft);
|
|
66
|
+
if (!review.ok) {
|
|
67
|
+
input.store.recordComment({
|
|
68
|
+
platform: post.platform,
|
|
69
|
+
commentId: comment.id,
|
|
70
|
+
accountId: input.account.id,
|
|
71
|
+
action: "blocked",
|
|
72
|
+
tone: decision.tone,
|
|
73
|
+
replyText: null,
|
|
74
|
+
at: input.now,
|
|
75
|
+
commentText: comment.text,
|
|
76
|
+
dryRun: input.dryRun,
|
|
77
|
+
});
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (!input.dryRun) {
|
|
81
|
+
const token = await input.tokenFor(post.platform);
|
|
82
|
+
try {
|
|
83
|
+
await sendReply(input.fetchImpl, input.config, post.platform, token, comment, draft.text);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
if (!(error instanceof ReplyRefused)) throw error;
|
|
86
|
+
// A deleted comment or closed comments will not change. Record it so it is never tried again.
|
|
87
|
+
input.store.recordComment({
|
|
88
|
+
platform: post.platform,
|
|
89
|
+
commentId: comment.id,
|
|
90
|
+
accountId: input.account.id,
|
|
91
|
+
action: "skipped",
|
|
92
|
+
tone: decision.tone,
|
|
93
|
+
replyText: null,
|
|
94
|
+
at: input.now,
|
|
95
|
+
code: error.code,
|
|
96
|
+
commentText: comment.text,
|
|
97
|
+
});
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
input.store.recordComment({
|
|
102
|
+
platform: post.platform,
|
|
103
|
+
commentId: comment.id,
|
|
104
|
+
accountId: input.account.id,
|
|
105
|
+
action: "replied",
|
|
106
|
+
tone: decision.tone,
|
|
107
|
+
replyText: draft.text,
|
|
108
|
+
at: input.now,
|
|
109
|
+
commentText: comment.text,
|
|
110
|
+
dryRun: input.dryRun,
|
|
111
|
+
});
|
|
112
|
+
replied += 1;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return replied;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface CommentItem {
|
|
119
|
+
id: string;
|
|
120
|
+
text: string;
|
|
121
|
+
videoTitle: string;
|
|
122
|
+
parentId: string | null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** The platform refused this one reply for a reason that will not change. */
|
|
126
|
+
class ReplyRefused extends Error {
|
|
127
|
+
constructor(readonly code: string, message: string) {
|
|
128
|
+
super(message);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const QUOTA_REASON = /^(quotaExceeded|rateLimitExceeded|dailyLimitExceeded|userRateLimitExceeded)$/;
|
|
133
|
+
|
|
134
|
+
function googleReason(json: unknown): string {
|
|
135
|
+
return String((json as { error?: { errors?: Array<{ reason?: string }> } } | null)?.error?.errors?.[0]?.reason ?? "");
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function listComments(
|
|
139
|
+
input: { config: AppConfig; fetchImpl: typeof fetch; tokenFor: (platform: OwnedPost["platform"]) => Promise<string>; dryRun: boolean },
|
|
140
|
+
post: OwnedPost,
|
|
141
|
+
): Promise<CommentItem[]> {
|
|
142
|
+
if (input.dryRun) return [];
|
|
143
|
+
const token = await input.tokenFor(post.platform);
|
|
144
|
+
if (post.platform === "youtube") {
|
|
145
|
+
const url =
|
|
146
|
+
`https://www.googleapis.com/youtube/v3/commentThreads?part=snippet&videoId=${encodeURIComponent(post.externalId)}` +
|
|
147
|
+
`&maxResults=20&order=time&textFormat=plainText`;
|
|
148
|
+
const result = await requestJson(input.fetchImpl, url, { headers: { authorization: `Bearer ${token}` } });
|
|
149
|
+
if (result.status === 401) throw new ConfigError("youtube_token", "YouTube token is expired or revoked");
|
|
150
|
+
if (result.status === 403 || result.status === 400) {
|
|
151
|
+
const reason = googleReason(result.json);
|
|
152
|
+
if (QUOTA_REASON.test(reason)) throw new RetryableError("youtube_comments", "YouTube comment quota", 60 * 60_000);
|
|
153
|
+
// Comments are off or this video is gone: there is nothing to answer.
|
|
154
|
+
if (reason === "commentsDisabled" || reason === "videoNotFound") return [];
|
|
155
|
+
throw new RetryableError("youtube_comments", `YouTube comment read HTTP ${result.status}`, 60 * 60_000);
|
|
156
|
+
}
|
|
157
|
+
const items = (result.json as { items?: Array<{ id?: string; snippet?: { topLevelComment?: { id?: string; snippet?: { textDisplay?: string } } } }> }).items ?? [];
|
|
158
|
+
return items.map((item) => ({
|
|
159
|
+
id: item.snippet?.topLevelComment?.id ?? item.id ?? "",
|
|
160
|
+
text: item.snippet?.topLevelComment?.snippet?.textDisplay ?? "",
|
|
161
|
+
videoTitle: post.externalId,
|
|
162
|
+
parentId: null,
|
|
163
|
+
})).filter((item) => item.id && item.text);
|
|
164
|
+
}
|
|
165
|
+
const version = input.config.meta.graphVersion;
|
|
166
|
+
const result = await requestJson(
|
|
167
|
+
input.fetchImpl,
|
|
168
|
+
`https://graph.facebook.com/${version}/${post.externalId}/comments?fields=id,message&limit=20`,
|
|
169
|
+
{ headers: { authorization: `Bearer ${token}` } },
|
|
170
|
+
);
|
|
171
|
+
if (result.status === 429) throw new RetryableError("meta_comments", "Comment read rate limit");
|
|
172
|
+
const data = (result.json as { data?: Array<{ id?: string; message?: string }> }).data ?? [];
|
|
173
|
+
return data
|
|
174
|
+
.map((item) => ({ id: item.id ?? "", text: item.message ?? "", videoTitle: post.externalId, parentId: null }))
|
|
175
|
+
.filter((item) => item.id && item.text);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function sendReply(
|
|
179
|
+
fetchImpl: typeof fetch,
|
|
180
|
+
config: AppConfig,
|
|
181
|
+
platform: OwnedPost["platform"],
|
|
182
|
+
token: string,
|
|
183
|
+
comment: CommentItem,
|
|
184
|
+
message: string,
|
|
185
|
+
): Promise<void> {
|
|
186
|
+
if (platform === "youtube") {
|
|
187
|
+
const result = await requestJson(fetchImpl, "https://www.googleapis.com/youtube/v3/comments?part=snippet", {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json" },
|
|
190
|
+
body: JSON.stringify({ snippet: { parentId: comment.id, textOriginal: message } }),
|
|
191
|
+
});
|
|
192
|
+
if (result.status === 401) throw new ConfigError("youtube_token", "YouTube token is expired or revoked");
|
|
193
|
+
if (result.status === 429 || result.status >= 500) throw new RetryableError("youtube_reply", `YouTube reply HTTP ${result.status}`);
|
|
194
|
+
if (result.status >= 400) {
|
|
195
|
+
if (QUOTA_REASON.test(googleReason(result.json))) {
|
|
196
|
+
throw new RetryableError("youtube_reply", "YouTube reply quota", 30 * 60_000);
|
|
197
|
+
}
|
|
198
|
+
throw new ReplyRefused(`youtube_reply_${result.status}`, `YouTube reply HTTP ${result.status}`);
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
const version = config.meta.graphVersion;
|
|
203
|
+
const path = platform === "instagram" ? `${comment.id}/replies` : `${comment.id}/comments`;
|
|
204
|
+
const result = await requestJson(fetchImpl, `https://graph.facebook.com/${version}/${path}`, {
|
|
205
|
+
method: "POST",
|
|
206
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/x-www-form-urlencoded" },
|
|
207
|
+
body: new URLSearchParams({ message }),
|
|
208
|
+
});
|
|
209
|
+
if (result.status === 401) throw new ConfigError("meta_token", "Meta token is expired or invalid");
|
|
210
|
+
if (result.status === 429 || result.status >= 500) throw new RetryableError("meta_reply", `Reply HTTP ${result.status}`);
|
|
211
|
+
if (result.status >= 400) throw new ReplyRefused(`${platform}_reply_${result.status}`, `Reply HTTP ${result.status}`);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function extractJson(text: string): string {
|
|
215
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/);
|
|
216
|
+
return (fenced?.[1] ?? text).trim();
|
|
217
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { QualityError, RetryableError } from "@vfrey/contracts";
|
|
2
|
+
import { assertNoAdvicePersona, assertNotVerbatim } from "@vfrey/contracts";
|
|
3
|
+
import { assertSubjectBinding, fitBeats } from "@vfrey/contracts";
|
|
4
|
+
import type { Beat, Candidate, ContentPackage, VariationPlan } from "@vfrey/contracts";
|
|
5
|
+
import { parseModelJson, type LlmClient } from "../llm/client.js";
|
|
6
|
+
|
|
7
|
+
export async function writePackage(input: {
|
|
8
|
+
llm: LlmClient;
|
|
9
|
+
candidate: Candidate;
|
|
10
|
+
plan: VariationPlan;
|
|
11
|
+
niche: string;
|
|
12
|
+
language: string;
|
|
13
|
+
hashtagPool: string[];
|
|
14
|
+
}): Promise<Omit<ContentPackage, "voiceoverPath" | "outputPath" | "durationSec"> & { beats: Beat[]; durationSec: number }> {
|
|
15
|
+
let lastError: Error | null = null;
|
|
16
|
+
for (let attempt = 1; attempt <= 3; attempt += 1) {
|
|
17
|
+
const raw = await input.llm.complete({
|
|
18
|
+
system: [
|
|
19
|
+
"You write an original short-form video. Return JSON only.",
|
|
20
|
+
"The viewer value has to be specific to this source. Do not write a generic list that could fit any topic.",
|
|
21
|
+
"Do not copy 12 or more consecutive words from the source.",
|
|
22
|
+
"Do not role-play a doctor, lawyer, or financial adviser.",
|
|
23
|
+
"Do not claim the footage is a recording of a real event.",
|
|
24
|
+
`Niche: ${input.niche}. Language: ${input.language}.`,
|
|
25
|
+
`Hook pattern: ${input.plan.hookPattern}. Structure: ${input.plan.structure}.`,
|
|
26
|
+
].join("\n"),
|
|
27
|
+
user: [
|
|
28
|
+
"TASK:PACKAGE",
|
|
29
|
+
`SOURCE_TITLE: ${input.candidate.title}`,
|
|
30
|
+
`SOURCE_EXCERPT: ${input.candidate.excerpt}`,
|
|
31
|
+
`HOOK_PATTERN: ${input.plan.hookPattern}`,
|
|
32
|
+
`STRUCTURE: ${input.plan.structure}`,
|
|
33
|
+
`HASHTAG_STRATEGY: ${input.plan.hashtagStrategy}`,
|
|
34
|
+
lastError ? `PREVIOUS_DRAFT_REJECTED: ${lastError.message}` : "",
|
|
35
|
+
"JSON keys: title, hook, caption, description, hashtags, thumbnailPrompt, imagePrompts, beats.",
|
|
36
|
+
"beats is an array of {role, text, caption, seconds}. Roles: hook, setup, turn, evidence, payoff, cta. At least 4 beats.",
|
|
37
|
+
].join("\n"),
|
|
38
|
+
temperature: 0.7,
|
|
39
|
+
maxTokens: 1800,
|
|
40
|
+
});
|
|
41
|
+
try {
|
|
42
|
+
const parsed = parseModelJson(raw) as Record<string, unknown>;
|
|
43
|
+
const beats = fitBeats(asBeats(parsed.beats), input.plan.templateId);
|
|
44
|
+
const hook = String(parsed.hook ?? beats[0]?.text ?? "").trim();
|
|
45
|
+
const title = String(parsed.title ?? "").trim();
|
|
46
|
+
if (!title || !hook) throw new QualityError("thin_script", "Model omitted a title or hook.");
|
|
47
|
+
const script = beats.map((beat) => beat.text).join(" ");
|
|
48
|
+
assertSubjectBinding(script, input.candidate.title);
|
|
49
|
+
assertNotVerbatim(`${title} ${script}`, `${input.candidate.title} ${input.candidate.excerpt}`);
|
|
50
|
+
assertNoAdvicePersona(script);
|
|
51
|
+
const hashtags = normalizeHashtags(
|
|
52
|
+
Array.isArray(parsed.hashtags) ? parsed.hashtags.map(String) : [],
|
|
53
|
+
input.hashtagPool,
|
|
54
|
+
input.plan.hashtagStrategy,
|
|
55
|
+
);
|
|
56
|
+
const durationSec = round1(beats.reduce((sum, beat) => sum + beat.seconds, 0));
|
|
57
|
+
return {
|
|
58
|
+
candidate: input.candidate,
|
|
59
|
+
plan: input.plan,
|
|
60
|
+
title,
|
|
61
|
+
hook,
|
|
62
|
+
beats,
|
|
63
|
+
caption: String(parsed.caption ?? hook),
|
|
64
|
+
description: String(parsed.description ?? script),
|
|
65
|
+
hashtags,
|
|
66
|
+
thumbnailPrompt: String(parsed.thumbnailPrompt ?? title),
|
|
67
|
+
imagePrompts: Array.isArray(parsed.imagePrompts) ? parsed.imagePrompts.map(String).slice(0, 4) : [],
|
|
68
|
+
voiceoverScript: beats.map((beat) => beat.text).join("\n"),
|
|
69
|
+
durationSec,
|
|
70
|
+
};
|
|
71
|
+
} catch (error) {
|
|
72
|
+
lastError = error instanceof Error ? error : new Error(String(error));
|
|
73
|
+
if (error instanceof RetryableError) throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw lastError ?? new QualityError("generation", "Generation failed three times.");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function asBeats(value: unknown): Array<{ role: string; text: string; caption: string; seconds: number }> {
|
|
80
|
+
if (!Array.isArray(value)) throw new QualityError("thin_script", "Model returned no beats.");
|
|
81
|
+
return value.map((beat) => {
|
|
82
|
+
const row = beat as Record<string, unknown>;
|
|
83
|
+
return {
|
|
84
|
+
role: String(row.role ?? "turn"),
|
|
85
|
+
text: String(row.text ?? ""),
|
|
86
|
+
caption: String(row.caption ?? row.text ?? ""),
|
|
87
|
+
seconds: typeof row.seconds === "number" ? row.seconds : 3,
|
|
88
|
+
};
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function normalizeHashtags(modelTags: string[], pool: string[], strategy: string): string[] {
|
|
93
|
+
const cleaned = modelTags
|
|
94
|
+
.map((tag) => tag.trim().replace(/\s+/g, ""))
|
|
95
|
+
.map((tag) => (tag.startsWith("#") ? tag : `#${tag}`))
|
|
96
|
+
.filter((tag) => tag.length > 1);
|
|
97
|
+
const extra = strategy === "plain-language" ? pool.slice(0, 1) : pool.slice(1, 2);
|
|
98
|
+
const merged = [...cleaned, ...extra.map((tag) => (tag.startsWith("#") ? tag : `#${tag}`))];
|
|
99
|
+
const unique: string[] = [];
|
|
100
|
+
for (const tag of merged) {
|
|
101
|
+
if (!unique.some((item) => item.toLowerCase() === tag.toLowerCase())) unique.push(tag);
|
|
102
|
+
if (unique.length >= 6) break;
|
|
103
|
+
}
|
|
104
|
+
if (!unique.some((tag) => tag.toLowerCase() === "#shorts")) unique.push("#Shorts");
|
|
105
|
+
return unique.slice(0, 8);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function round1(value: number): number {
|
|
109
|
+
return Math.round(value * 10) / 10;
|
|
110
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { fitBeats } from "@vfrey/contracts";
|
|
2
|
+
import { assertNoAdvicePersona, assertNotVerbatim, assertSubjectBinding } from "@vfrey/contracts";
|
|
3
|
+
import type { Beat, Candidate, ContentPackage, VariationPlan } from "@vfrey/contracts";
|
|
4
|
+
import { normalizeHashtags } from "./index.js";
|
|
5
|
+
|
|
6
|
+
/** A script the coding agent wrote itself. `tick --script FILE` reads one of these instead of calling a model. */
|
|
7
|
+
export interface ScriptFile {
|
|
8
|
+
title: string;
|
|
9
|
+
hook: string;
|
|
10
|
+
lines: string[];
|
|
11
|
+
hashtags: string[];
|
|
12
|
+
subject?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const SCRIPT_LIMITS = { titleMax: 100, linesMin: 4, linesMax: 12, lineMax: 220, hashtagsMax: 8 } as const;
|
|
16
|
+
|
|
17
|
+
export type ScriptCheck = { ok: true; script: ScriptFile } | { ok: false; field: string; message: string; fix: string };
|
|
18
|
+
|
|
19
|
+
/** The JSON `npx vfrey tick --script-template` prints. */
|
|
20
|
+
export function scriptTemplate(): string {
|
|
21
|
+
const example: ScriptFile = {
|
|
22
|
+
title: "Why honey never spoils",
|
|
23
|
+
hook: "Honey never spoils, and the reason is plain chemistry.",
|
|
24
|
+
lines: [
|
|
25
|
+
"Honey never spoils, and the reason is plain chemistry.",
|
|
26
|
+
"It holds very little water, so microbes cannot grow in it.",
|
|
27
|
+
"It is also acidic, which most bacteria dislike.",
|
|
28
|
+
"As honey ripens, an enzyme called glucose oxidase makes a little hydrogen peroxide.",
|
|
29
|
+
"And the bees seal the comb, so no new moisture gets in.",
|
|
30
|
+
"That is why honey never spoils. It can still crystallize, but crystals are not spoilage.",
|
|
31
|
+
],
|
|
32
|
+
hashtags: ["#honey", "#science", "#facts"],
|
|
33
|
+
subject: "why honey never spoils",
|
|
34
|
+
};
|
|
35
|
+
return JSON.stringify(example, null, 2) + "\n";
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function bad(field: string, message: string, fix: string): ScriptCheck {
|
|
39
|
+
return { ok: false, field, message, fix };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Checks the agent's JSON. The first bad field is named, with the way to fix it. */
|
|
43
|
+
export function validateScript(text: string): ScriptCheck {
|
|
44
|
+
let raw: unknown;
|
|
45
|
+
try {
|
|
46
|
+
raw = JSON.parse(text);
|
|
47
|
+
} catch (error) {
|
|
48
|
+
return bad("file", `The script file is not valid JSON (${error instanceof Error ? error.message : String(error)}).`, "write one JSON object; `npx vfrey tick --script-template` prints an example.");
|
|
49
|
+
}
|
|
50
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
51
|
+
return bad("file", "The script file must hold one JSON object.", "see `npx vfrey tick --script-template`.");
|
|
52
|
+
}
|
|
53
|
+
const body = raw as Record<string, unknown>;
|
|
54
|
+
const { titleMax, linesMin, linesMax, lineMax, hashtagsMax } = SCRIPT_LIMITS;
|
|
55
|
+
const title = typeof body.title === "string" ? body.title.trim() : "";
|
|
56
|
+
if (!title) return bad("title", "title is required.", `set "title" to a short string of ${titleMax} characters or fewer.`);
|
|
57
|
+
if (title.length > titleMax) return bad("title", `title is ${title.length} characters; the limit is ${titleMax}.`, `shorten "title" to ${titleMax} characters or fewer.`);
|
|
58
|
+
const hook = typeof body.hook === "string" ? body.hook.trim() : "";
|
|
59
|
+
if (!hook) return bad("hook", "hook is required.", 'set "hook" to the opening sentence of the video.');
|
|
60
|
+
if (!Array.isArray(body.lines)) return bad("lines", "lines must be an array of spoken lines.", `set "lines" to ${linesMin} to ${linesMax} strings.`);
|
|
61
|
+
if (body.lines.length < linesMin || body.lines.length > linesMax) {
|
|
62
|
+
return bad("lines", `lines has ${body.lines.length} entries; it needs ${linesMin} to ${linesMax}.`, `write ${linesMin} to ${linesMax} short spoken lines (6 to 10 is best).`);
|
|
63
|
+
}
|
|
64
|
+
const lines: string[] = [];
|
|
65
|
+
for (const [index, line] of body.lines.entries()) {
|
|
66
|
+
const value = typeof line === "string" ? line.trim() : "";
|
|
67
|
+
if (!value) return bad(`lines[${index}]`, `lines[${index}] must be a non-empty string.`, "remove the empty line or write it.");
|
|
68
|
+
if (value.length > lineMax) {
|
|
69
|
+
return bad(`lines[${index}]`, `lines[${index}] is ${value.length} characters; the limit is ${lineMax}.`, `split it into shorter spoken lines of ${lineMax} characters or fewer.`);
|
|
70
|
+
}
|
|
71
|
+
lines.push(value);
|
|
72
|
+
}
|
|
73
|
+
if (!Array.isArray(body.hashtags) || body.hashtags.length < 1 || body.hashtags.length > hashtagsMax) {
|
|
74
|
+
const found = Array.isArray(body.hashtags) ? body.hashtags.length : 0;
|
|
75
|
+
return bad("hashtags", `hashtags has ${found} entries; it needs 1 to ${hashtagsMax}.`, `set "hashtags" to 1 to ${hashtagsMax} strings that start with #.`);
|
|
76
|
+
}
|
|
77
|
+
const hashtags: string[] = [];
|
|
78
|
+
for (const [index, tag] of body.hashtags.entries()) {
|
|
79
|
+
const value = typeof tag === "string" ? tag.trim() : "";
|
|
80
|
+
if (!/^#\S+$/.test(value)) return bad(`hashtags[${index}]`, `hashtags[${index}] must start with # and have no spaces.`, 'write each hashtag like "#honey".');
|
|
81
|
+
hashtags.push(value);
|
|
82
|
+
}
|
|
83
|
+
const script: ScriptFile = { title, hook, lines, hashtags };
|
|
84
|
+
if (body.subject !== undefined) {
|
|
85
|
+
const subject = typeof body.subject === "string" ? body.subject.trim() : "";
|
|
86
|
+
if (!subject) return bad("subject", "subject must be a non-empty string when it is set.", 'remove "subject" or set it to the topic.');
|
|
87
|
+
script.subject = subject;
|
|
88
|
+
}
|
|
89
|
+
return { ok: true, script };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const MIDDLE_ROLES = ["setup", "turn", "evidence", "payoff"];
|
|
93
|
+
const MAX_BEATS = 8;
|
|
94
|
+
|
|
95
|
+
/** Spoken lines to beats. More than eight lines share beats, because a film holds eight; the voice still reads every line. */
|
|
96
|
+
export function linesToBeats(lines: string[]): Array<{ role: string; text: string; caption: string; seconds: number }> {
|
|
97
|
+
const groups: string[][] = lines.map((line) => [line]);
|
|
98
|
+
while (groups.length > MAX_BEATS) {
|
|
99
|
+
// Join the neighbouring pair with the fewest words.
|
|
100
|
+
let at = 0;
|
|
101
|
+
let least = Infinity;
|
|
102
|
+
for (let i = 0; i < groups.length - 1; i += 1) {
|
|
103
|
+
const size = groups[i]!.join(" ").length + groups[i + 1]!.join(" ").length;
|
|
104
|
+
if (size < least) {
|
|
105
|
+
least = size;
|
|
106
|
+
at = i;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
groups.splice(at, 2, [...groups[at]!, ...groups[at + 1]!]);
|
|
110
|
+
}
|
|
111
|
+
return groups.map((group, index) => {
|
|
112
|
+
const text = group.join(" ");
|
|
113
|
+
const words = text.split(/\s+/).filter(Boolean).length;
|
|
114
|
+
return {
|
|
115
|
+
role: index === 0 ? "hook" : index === groups.length - 1 ? "payoff" : MIDDLE_ROLES[(index - 1) % 3]!,
|
|
116
|
+
text,
|
|
117
|
+
caption: text,
|
|
118
|
+
seconds: Math.max(2, Math.round((words / 2.6) * 10) / 10),
|
|
119
|
+
};
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** The content package for an agent-written script, built the way the writer builds one. */
|
|
124
|
+
export function packageFromScript(input: {
|
|
125
|
+
script: ScriptFile;
|
|
126
|
+
candidate: Candidate;
|
|
127
|
+
plan: VariationPlan;
|
|
128
|
+
hashtagPool: string[];
|
|
129
|
+
}): Omit<ContentPackage, "voiceoverPath" | "outputPath"> & { beats: Beat[] } {
|
|
130
|
+
const { script, candidate, plan } = input;
|
|
131
|
+
const beats = fitBeats(linesToBeats(script.lines), plan.templateId);
|
|
132
|
+
const spoken = script.lines.join("\n");
|
|
133
|
+
const text = script.lines.join(" ");
|
|
134
|
+
assertSubjectBinding(text, candidate.title);
|
|
135
|
+
assertNotVerbatim(`${script.title} ${text}`, `${candidate.title} ${candidate.excerpt}`);
|
|
136
|
+
assertNoAdvicePersona(text);
|
|
137
|
+
const durationSec = Math.round(beats.reduce((sum, beat) => sum + beat.seconds, 0) * 10) / 10;
|
|
138
|
+
return {
|
|
139
|
+
candidate,
|
|
140
|
+
plan,
|
|
141
|
+
title: script.title,
|
|
142
|
+
hook: script.hook,
|
|
143
|
+
beats,
|
|
144
|
+
caption: script.hook,
|
|
145
|
+
description: text,
|
|
146
|
+
hashtags: normalizeHashtags(script.hashtags, input.hashtagPool, plan.hashtagStrategy),
|
|
147
|
+
thumbnailPrompt: script.title,
|
|
148
|
+
imagePrompts: [],
|
|
149
|
+
voiceoverScript: spoken,
|
|
150
|
+
durationSec,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { copyTokens, significantTokens } from "@vfrey/contracts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Longest title the platforms accept, and the longest run of words the verbatim gate lets through (12).
|
|
5
|
+
* The gate counts tokens, not spaces ("Board's" is two, "17," is one), so the cap is on both.
|
|
6
|
+
*/
|
|
7
|
+
export const TITLE_MAX_CHARS = 100;
|
|
8
|
+
export const TITLE_MAX_WORDS = 11;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* A source title or `--about` subject, made fit to be a video title: whitespace collapsed, trailing
|
|
12
|
+
* punctuation dropped, first letter capitalised, and cut at a word boundary to 100 characters (and 11 words,
|
|
13
|
+
* so a long title never trips the 12-word verbatim-copy gate).
|
|
14
|
+
*/
|
|
15
|
+
export function cleanTitle(value: string): string {
|
|
16
|
+
let text = value.replace(/\s+/g, " ").trim();
|
|
17
|
+
text = stripEdges(text);
|
|
18
|
+
const kept: string[] = [];
|
|
19
|
+
let tokenCount = 0;
|
|
20
|
+
for (const word of text.split(" ").filter(Boolean)) {
|
|
21
|
+
tokenCount += copyTokens(word).length;
|
|
22
|
+
if (kept.length >= TITLE_MAX_WORDS || tokenCount > TITLE_MAX_WORDS) break;
|
|
23
|
+
kept.push(word);
|
|
24
|
+
}
|
|
25
|
+
text = kept.join(" ");
|
|
26
|
+
const chars = Array.from(text);
|
|
27
|
+
if (chars.length > TITLE_MAX_CHARS) {
|
|
28
|
+
const cut = chars.slice(0, TITLE_MAX_CHARS);
|
|
29
|
+
const boundary = chars[TITLE_MAX_CHARS] === " " ? TITLE_MAX_CHARS : cut.lastIndexOf(" ");
|
|
30
|
+
text = (boundary > 0 ? cut.slice(0, boundary) : cut).join("");
|
|
31
|
+
}
|
|
32
|
+
text = stripEdges(text);
|
|
33
|
+
const first = Array.from(text)[0];
|
|
34
|
+
if (first && first.toUpperCase() !== first) text = first.toUpperCase() + text.slice(first.length);
|
|
35
|
+
return text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripEdges(value: string): string {
|
|
39
|
+
let text = value.replace(/[\s:;,.\-–—]+$/u, "").trim();
|
|
40
|
+
// A title wrapped whole in matching quotes loses them.
|
|
41
|
+
const pairs: Array<[string, string]> = [['"', '"'], ["'", "'"], ["“", "”"], ["‘", "’"]];
|
|
42
|
+
for (const [open, close] of pairs) {
|
|
43
|
+
if (text.length > 2 && text.startsWith(open) && text.endsWith(close) && !text.slice(1, -1).includes(open)) {
|
|
44
|
+
text = text.slice(1, -1).replace(/[\s:;,.\-–—]+$/u, "").trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return text;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** One hashtag made from the subject: its first word of four letters or more (else its first word). Null when it has none. */
|
|
51
|
+
export function subjectTag(subject: string): string | null {
|
|
52
|
+
const words = subject.split(/\s+/).map((word) => word.replace(/[^\p{L}\p{N}]/gu, "")).filter(Boolean);
|
|
53
|
+
const word = words.find((item) => Array.from(item).length >= 4) ?? words[0];
|
|
54
|
+
return word ? `#${word.toLowerCase()}` : null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The subject with a lowercase first letter, for use inside a sentence. Acronyms and mixed-case names are left alone. */
|
|
58
|
+
export function lowerFirst(subject: string): string {
|
|
59
|
+
const first = subject.split(/\s+/)[0] ?? "";
|
|
60
|
+
if (/\p{Lu}/u.test(first.slice(1))) return subject;
|
|
61
|
+
const char = Array.from(subject)[0];
|
|
62
|
+
return char ? char.toLowerCase() + subject.slice(char.length) : subject;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The shortest form of the subject that still binds a script to it: the first two significant words
|
|
67
|
+
* (the gate needs two), lowercase, joined with "and". Falls back to the lowercase-initial subject.
|
|
68
|
+
*/
|
|
69
|
+
export function subjectGist(subject: string): string {
|
|
70
|
+
const words = Array.from(new Set(significantTokens(subject))).slice(0, 2);
|
|
71
|
+
return words.length > 0 ? words.join(" and ") : lowerFirst(subject);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** A title for the stand-in writer: the cleaned subject plus ", briefly" when that fits in 100 characters. */
|
|
75
|
+
export function standInTitle(subject: string): string {
|
|
76
|
+
const withTail = `${subject}, briefly`;
|
|
77
|
+
return Array.from(withTail).length <= TITLE_MAX_CHARS ? withTail : subject;
|
|
78
|
+
}
|