@kenkaiiii/gg-editor 0.6.7 → 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +64 -1
- package/dist/cli.js.map +1 -1
- package/dist/core/audio-mix.d.ts.map +1 -1
- package/dist/core/audio-mix.js +8 -1
- package/dist/core/audio-mix.js.map +1 -1
- package/dist/core/audio-mix.test.js +1 -1
- package/dist/core/audio-mix.test.js.map +1 -1
- package/dist/core/auth/api-keys.d.ts +1 -1
- package/dist/core/auth/api-keys.d.ts.map +1 -1
- package/dist/core/auth/api-keys.js +2 -1
- package/dist/core/auth/api-keys.js.map +1 -1
- package/dist/core/auth/index.d.ts +18 -5
- package/dist/core/auth/index.d.ts.map +1 -1
- package/dist/core/auth/index.js +16 -4
- package/dist/core/auth/index.js.map +1 -1
- package/dist/core/auth/login.d.ts.map +1 -1
- package/dist/core/auth/login.js +4 -31
- package/dist/core/auth/login.js.map +1 -1
- package/dist/core/beats.d.ts +59 -0
- package/dist/core/beats.d.ts.map +1 -0
- package/dist/core/beats.js +122 -0
- package/dist/core/beats.js.map +1 -0
- package/dist/core/beats.test.d.ts +2 -0
- package/dist/core/beats.test.d.ts.map +1 -0
- package/dist/core/beats.test.js +86 -0
- package/dist/core/beats.test.js.map +1 -0
- package/dist/core/brand-kit.d.ts +80 -0
- package/dist/core/brand-kit.d.ts.map +1 -0
- package/dist/core/brand-kit.js +96 -0
- package/dist/core/brand-kit.js.map +1 -0
- package/dist/core/brand-kit.test.d.ts +2 -0
- package/dist/core/brand-kit.test.d.ts.map +1 -0
- package/dist/core/brand-kit.test.js +76 -0
- package/dist/core/brand-kit.test.js.map +1 -0
- package/dist/core/bundled-sfx.d.ts +64 -0
- package/dist/core/bundled-sfx.d.ts.map +1 -0
- package/dist/core/bundled-sfx.js +218 -0
- package/dist/core/bundled-sfx.js.map +1 -0
- package/dist/core/bundled-sfx.test.d.ts +2 -0
- package/dist/core/bundled-sfx.test.d.ts.map +1 -0
- package/dist/core/bundled-sfx.test.js +81 -0
- package/dist/core/bundled-sfx.test.js.map +1 -0
- package/dist/core/child-abort.d.ts +57 -0
- package/dist/core/child-abort.d.ts.map +1 -0
- package/dist/core/child-abort.js +95 -0
- package/dist/core/child-abort.js.map +1 -0
- package/dist/core/child-abort.test.d.ts +2 -0
- package/dist/core/child-abort.test.d.ts.map +1 -0
- package/dist/core/child-abort.test.js +88 -0
- package/dist/core/child-abort.test.js.map +1 -0
- package/dist/core/clip-scoring.d.ts +44 -0
- package/dist/core/clip-scoring.d.ts.map +1 -0
- package/dist/core/clip-scoring.js +165 -0
- package/dist/core/clip-scoring.js.map +1 -0
- package/dist/core/clip-scoring.test.d.ts +2 -0
- package/dist/core/clip-scoring.test.d.ts.map +1 -0
- package/dist/core/clip-scoring.test.js +113 -0
- package/dist/core/clip-scoring.test.js.map +1 -0
- package/dist/core/emoji-captions.d.ts +45 -0
- package/dist/core/emoji-captions.d.ts.map +1 -0
- package/dist/core/emoji-captions.js +121 -0
- package/dist/core/emoji-captions.js.map +1 -0
- package/dist/core/face-reframe.d.ts +91 -0
- package/dist/core/face-reframe.d.ts.map +1 -0
- package/dist/core/face-reframe.js +141 -0
- package/dist/core/face-reframe.js.map +1 -0
- package/dist/core/face-reframe.test.d.ts +2 -0
- package/dist/core/face-reframe.test.d.ts.map +1 -0
- package/dist/core/face-reframe.test.js +171 -0
- package/dist/core/face-reframe.test.js.map +1 -0
- package/dist/core/filler-words.d.ts +57 -9
- package/dist/core/filler-words.d.ts.map +1 -1
- package/dist/core/filler-words.js +61 -9
- package/dist/core/filler-words.js.map +1 -1
- package/dist/core/filler-words.test.js +91 -17
- package/dist/core/filler-words.test.js.map +1 -1
- package/dist/core/hook-rewrite.d.ts +48 -0
- package/dist/core/hook-rewrite.d.ts.map +1 -0
- package/dist/core/hook-rewrite.js +151 -0
- package/dist/core/hook-rewrite.js.map +1 -0
- package/dist/core/hook-rewrite.test.d.ts +2 -0
- package/dist/core/hook-rewrite.test.d.ts.map +1 -0
- package/dist/core/hook-rewrite.test.js +58 -0
- package/dist/core/hook-rewrite.test.js.map +1 -0
- package/dist/core/hosts/lazy.d.ts.map +1 -1
- package/dist/core/hosts/lazy.js +2 -0
- package/dist/core/hosts/lazy.js.map +1 -1
- package/dist/core/hosts/premiere/adapter.d.ts +1 -0
- package/dist/core/hosts/premiere/adapter.d.ts.map +1 -1
- package/dist/core/hosts/premiere/adapter.js.map +1 -1
- package/dist/core/hosts/premiere/bridge-source.d.ts.map +1 -1
- package/dist/core/hosts/premiere/bridge-source.js +6 -3
- package/dist/core/hosts/premiere/bridge-source.js.map +1 -1
- package/dist/core/hosts/resolve/adapter.d.ts +1 -0
- package/dist/core/hosts/resolve/adapter.d.ts.map +1 -1
- package/dist/core/hosts/resolve/adapter.js.map +1 -1
- package/dist/core/hosts/resolve/bridge-source.d.ts.map +1 -1
- package/dist/core/hosts/resolve/bridge-source.js +31 -4
- package/dist/core/hosts/resolve/bridge-source.js.map +1 -1
- package/dist/core/hosts/resolve/bridge.d.ts +2 -19
- package/dist/core/hosts/resolve/bridge.d.ts.map +1 -1
- package/dist/core/hosts/resolve/bridge.js +70 -41
- package/dist/core/hosts/resolve/bridge.js.map +1 -1
- package/dist/core/hosts/resolve/bridge.test.js +130 -0
- package/dist/core/hosts/resolve/bridge.test.js.map +1 -1
- package/dist/core/hosts/types.d.ts +6 -0
- package/dist/core/hosts/types.d.ts.map +1 -1
- package/dist/core/hosts/types.js.map +1 -1
- package/dist/core/logger.d.ts +32 -0
- package/dist/core/logger.d.ts.map +1 -0
- package/dist/core/logger.js +188 -0
- package/dist/core/logger.js.map +1 -0
- package/dist/core/loop-match.d.ts +57 -0
- package/dist/core/loop-match.d.ts.map +1 -0
- package/dist/core/loop-match.js +91 -0
- package/dist/core/loop-match.js.map +1 -0
- package/dist/core/media/ffmpeg.d.ts.map +1 -1
- package/dist/core/media/ffmpeg.js +14 -3
- package/dist/core/media/ffmpeg.js.map +1 -1
- package/dist/core/multi-format.d.ts +67 -0
- package/dist/core/multi-format.d.ts.map +1 -0
- package/dist/core/multi-format.js +127 -0
- package/dist/core/multi-format.js.map +1 -0
- package/dist/core/multi-format.test.d.ts +2 -0
- package/dist/core/multi-format.test.d.ts.map +1 -0
- package/dist/core/multi-format.test.js +151 -0
- package/dist/core/multi-format.test.js.map +1 -0
- package/dist/core/python/beats.py +61 -0
- package/dist/core/python/face_reframe.py +163 -0
- package/dist/core/python/sidecar-path.d.ts +13 -0
- package/dist/core/python/sidecar-path.d.ts.map +1 -0
- package/dist/core/python/sidecar-path.js +24 -0
- package/dist/core/python/sidecar-path.js.map +1 -0
- package/dist/core/python.d.ts +57 -0
- package/dist/core/python.d.ts.map +1 -0
- package/dist/core/python.js +107 -0
- package/dist/core/python.js.map +1 -0
- package/dist/core/python.test.d.ts +2 -0
- package/dist/core/python.test.d.ts.map +1 -0
- package/dist/core/python.test.js +129 -0
- package/dist/core/python.test.js.map +1 -0
- package/dist/core/retention-structure.d.ts +81 -0
- package/dist/core/retention-structure.d.ts.map +1 -0
- package/dist/core/retention-structure.js +206 -0
- package/dist/core/retention-structure.js.map +1 -0
- package/dist/core/retention-structure.test.d.ts +2 -0
- package/dist/core/retention-structure.test.d.ts.map +1 -0
- package/dist/core/retention-structure.test.js +88 -0
- package/dist/core/retention-structure.test.js.map +1 -0
- package/dist/core/review.d.ts +17 -0
- package/dist/core/review.d.ts.map +1 -1
- package/dist/core/review.js +20 -24
- package/dist/core/review.js.map +1 -1
- package/dist/core/safe-paths.d.ts +11 -0
- package/dist/core/safe-paths.d.ts.map +1 -1
- package/dist/core/safe-paths.js +26 -10
- package/dist/core/safe-paths.js.map +1 -1
- package/dist/core/safe-paths.test.js +16 -0
- package/dist/core/safe-paths.test.js.map +1 -1
- package/dist/core/skills-loader.d.ts +48 -2
- package/dist/core/skills-loader.d.ts.map +1 -1
- package/dist/core/skills-loader.js +97 -19
- package/dist/core/skills-loader.js.map +1 -1
- package/dist/core/skills-loader.test.js +63 -1
- package/dist/core/skills-loader.test.js.map +1 -1
- package/dist/core/srt.d.ts +42 -7
- package/dist/core/srt.d.ts.map +1 -1
- package/dist/core/srt.js +101 -32
- package/dist/core/srt.js.map +1 -1
- package/dist/core/srt.test.js +54 -1
- package/dist/core/srt.test.js.map +1 -1
- package/dist/core/thumbnail-compose.d.ts +58 -0
- package/dist/core/thumbnail-compose.d.ts.map +1 -0
- package/dist/core/thumbnail-compose.js +101 -0
- package/dist/core/thumbnail-compose.js.map +1 -0
- package/dist/core/thumbnail-promise.d.ts +46 -0
- package/dist/core/thumbnail-promise.d.ts.map +1 -0
- package/dist/core/thumbnail-promise.js +133 -0
- package/dist/core/thumbnail-promise.js.map +1 -0
- package/dist/core/thumbnail-promise.test.d.ts +2 -0
- package/dist/core/thumbnail-promise.test.d.ts.map +1 -0
- package/dist/core/thumbnail-promise.test.js +52 -0
- package/dist/core/thumbnail-promise.test.js.map +1 -0
- package/dist/core/viral-moments.d.ts +70 -0
- package/dist/core/viral-moments.d.ts.map +1 -0
- package/dist/core/viral-moments.js +192 -0
- package/dist/core/viral-moments.js.map +1 -0
- package/dist/core/viral-moments.test.d.ts +2 -0
- package/dist/core/viral-moments.test.d.ts.map +1 -0
- package/dist/core/viral-moments.test.js +153 -0
- package/dist/core/viral-moments.test.js.map +1 -0
- package/dist/core/whisper.d.ts +16 -0
- package/dist/core/whisper.d.ts.map +1 -1
- package/dist/core/whisper.js +72 -5
- package/dist/core/whisper.js.map +1 -1
- package/dist/core/whisper.test.js +111 -1
- package/dist/core/whisper.test.js.map +1 -1
- package/dist/core/youtube-metadata.d.ts +44 -0
- package/dist/core/youtube-metadata.d.ts.map +1 -0
- package/dist/core/youtube-metadata.js +168 -0
- package/dist/core/youtube-metadata.js.map +1 -0
- package/dist/core/youtube-metadata.test.d.ts +2 -0
- package/dist/core/youtube-metadata.test.d.ts.map +1 -0
- package/dist/core/youtube-metadata.test.js +132 -0
- package/dist/core/youtube-metadata.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -2
- package/dist/index.js.map +1 -1
- package/dist/prompt-commands.d.ts +24 -0
- package/dist/prompt-commands.d.ts.map +1 -0
- package/dist/prompt-commands.js +243 -0
- package/dist/prompt-commands.js.map +1 -0
- package/dist/prompt-commands.test.d.ts +2 -0
- package/dist/prompt-commands.test.d.ts.map +1 -0
- package/dist/prompt-commands.test.js +46 -0
- package/dist/prompt-commands.test.js.map +1 -0
- package/dist/skills.d.ts +6 -6
- package/dist/skills.d.ts.map +1 -1
- package/dist/skills.js +1426 -445
- package/dist/skills.js.map +1 -1
- package/dist/system-prompt.d.ts.map +1 -1
- package/dist/system-prompt.js +108 -0
- package/dist/system-prompt.js.map +1 -1
- package/dist/tools/add-fades.d.ts.map +1 -1
- package/dist/tools/add-fades.js +2 -1
- package/dist/tools/add-fades.js.map +1 -1
- package/dist/tools/add-sfx-at-cuts.d.ts.map +1 -1
- package/dist/tools/add-sfx-at-cuts.js +36 -11
- package/dist/tools/add-sfx-at-cuts.js.map +1 -1
- package/dist/tools/add-sfx-to-timeline.d.ts +34 -0
- package/dist/tools/add-sfx-to-timeline.d.ts.map +1 -0
- package/dist/tools/add-sfx-to-timeline.js +169 -0
- package/dist/tools/add-sfx-to-timeline.js.map +1 -0
- package/dist/tools/add-sfx-to-timeline.test.d.ts +2 -0
- package/dist/tools/add-sfx-to-timeline.test.d.ts.map +1 -0
- package/dist/tools/add-sfx-to-timeline.test.js +181 -0
- package/dist/tools/add-sfx-to-timeline.test.js.map +1 -0
- package/dist/tools/audit-first-frame.d.ts +36 -0
- package/dist/tools/audit-first-frame.d.ts.map +1 -0
- package/dist/tools/audit-first-frame.js +181 -0
- package/dist/tools/audit-first-frame.js.map +1 -0
- package/dist/tools/audit-retention-structure.d.ts +20 -0
- package/dist/tools/audit-retention-structure.d.ts.map +1 -0
- package/dist/tools/audit-retention-structure.js +95 -0
- package/dist/tools/audit-retention-structure.js.map +1 -0
- package/dist/tools/audit-retention-structure.test.d.ts +2 -0
- package/dist/tools/audit-retention-structure.test.d.ts.map +1 -0
- package/dist/tools/audit-retention-structure.test.js +93 -0
- package/dist/tools/audit-retention-structure.test.js.map +1 -0
- package/dist/tools/bleep-words.d.ts +59 -0
- package/dist/tools/bleep-words.d.ts.map +1 -0
- package/dist/tools/bleep-words.js +211 -0
- package/dist/tools/bleep-words.js.map +1 -0
- package/dist/tools/bleep-words.test.d.ts +2 -0
- package/dist/tools/bleep-words.test.d.ts.map +1 -0
- package/dist/tools/bleep-words.test.js +96 -0
- package/dist/tools/bleep-words.test.js.map +1 -0
- package/dist/tools/burn-subtitles.d.ts.map +1 -1
- package/dist/tools/burn-subtitles.js +10 -5
- package/dist/tools/burn-subtitles.js.map +1 -1
- package/dist/tools/clean-audio.d.ts.map +1 -1
- package/dist/tools/clean-audio.js +2 -1
- package/dist/tools/clean-audio.js.map +1 -1
- package/dist/tools/cluster-takes.js +2 -1
- package/dist/tools/cluster-takes.js.map +1 -1
- package/dist/tools/compose-thumbnail-variants.d.ts +70 -0
- package/dist/tools/compose-thumbnail-variants.d.ts.map +1 -0
- package/dist/tools/compose-thumbnail-variants.js +274 -0
- package/dist/tools/compose-thumbnail-variants.js.map +1 -0
- package/dist/tools/compose-thumbnail.d.ts +6 -13
- package/dist/tools/compose-thumbnail.d.ts.map +1 -1
- package/dist/tools/compose-thumbnail.js +44 -81
- package/dist/tools/compose-thumbnail.js.map +1 -1
- package/dist/tools/concat-videos.d.ts.map +1 -1
- package/dist/tools/concat-videos.js +12 -5
- package/dist/tools/concat-videos.js.map +1 -1
- package/dist/tools/concat-videos.test.d.ts +2 -0
- package/dist/tools/concat-videos.test.d.ts.map +1 -0
- package/dist/tools/concat-videos.test.js +103 -0
- package/dist/tools/concat-videos.test.js.map +1 -0
- package/dist/tools/crossfade-videos.d.ts.map +1 -1
- package/dist/tools/crossfade-videos.js +2 -1
- package/dist/tools/crossfade-videos.js.map +1 -1
- package/dist/tools/cut-filler-words.d.ts.map +1 -1
- package/dist/tools/cut-filler-words.js +24 -8
- package/dist/tools/cut-filler-words.js.map +1 -1
- package/dist/tools/detect-speaker-changes.js +2 -1
- package/dist/tools/detect-speaker-changes.js.map +1 -1
- package/dist/tools/extract-audio.d.ts.map +1 -1
- package/dist/tools/extract-audio.js +13 -7
- package/dist/tools/extract-audio.js.map +1 -1
- package/dist/tools/face-reframe.d.ts +30 -0
- package/dist/tools/face-reframe.d.ts.map +1 -0
- package/dist/tools/face-reframe.js +143 -0
- package/dist/tools/face-reframe.js.map +1 -0
- package/dist/tools/face-reframe.test.d.ts +2 -0
- package/dist/tools/face-reframe.test.d.ts.map +1 -0
- package/dist/tools/face-reframe.test.js +139 -0
- package/dist/tools/face-reframe.test.js.map +1 -0
- package/dist/tools/find-viral-moments.d.ts +23 -0
- package/dist/tools/find-viral-moments.d.ts.map +1 -0
- package/dist/tools/find-viral-moments.js +176 -0
- package/dist/tools/find-viral-moments.js.map +1 -0
- package/dist/tools/find-viral-moments.test.d.ts +2 -0
- package/dist/tools/find-viral-moments.test.d.ts.map +1 -0
- package/dist/tools/find-viral-moments.test.js +144 -0
- package/dist/tools/find-viral-moments.test.js.map +1 -0
- package/dist/tools/generate-gif.d.ts.map +1 -1
- package/dist/tools/generate-gif.js +47 -40
- package/dist/tools/generate-gif.js.map +1 -1
- package/dist/tools/generate-gif.test.d.ts +2 -0
- package/dist/tools/generate-gif.test.d.ts.map +1 -0
- package/dist/tools/generate-gif.test.js +115 -0
- package/dist/tools/generate-gif.test.js.map +1 -0
- package/dist/tools/generate-outro.d.ts +18 -0
- package/dist/tools/generate-outro.d.ts.map +1 -0
- package/dist/tools/generate-outro.js +175 -0
- package/dist/tools/generate-outro.js.map +1 -0
- package/dist/tools/generate-youtube-metadata.d.ts +23 -0
- package/dist/tools/generate-youtube-metadata.d.ts.map +1 -0
- package/dist/tools/generate-youtube-metadata.js +103 -0
- package/dist/tools/generate-youtube-metadata.js.map +1 -0
- package/dist/tools/generate-youtube-metadata.test.d.ts +2 -0
- package/dist/tools/generate-youtube-metadata.test.d.ts.map +1 -0
- package/dist/tools/generate-youtube-metadata.test.js +118 -0
- package/dist/tools/generate-youtube-metadata.test.js.map +1 -0
- package/dist/tools/index.d.ts +14 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +130 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/index.test.js +27 -1
- package/dist/tools/index.test.js.map +1 -1
- package/dist/tools/ken-burns.d.ts.map +1 -1
- package/dist/tools/ken-burns.js +2 -1
- package/dist/tools/ken-burns.js.map +1 -1
- package/dist/tools/loop-match-short.d.ts +22 -0
- package/dist/tools/loop-match-short.d.ts.map +1 -0
- package/dist/tools/loop-match-short.js +107 -0
- package/dist/tools/loop-match-short.js.map +1 -0
- package/dist/tools/mix-audio.d.ts.map +1 -1
- package/dist/tools/mix-audio.js +2 -1
- package/dist/tools/mix-audio.js.map +1 -1
- package/dist/tools/normalize-loudness.d.ts.map +1 -1
- package/dist/tools/normalize-loudness.js +2 -1
- package/dist/tools/normalize-loudness.js.map +1 -1
- package/dist/tools/path-traversal.test.d.ts +15 -0
- package/dist/tools/path-traversal.test.d.ts.map +1 -0
- package/dist/tools/path-traversal.test.js +223 -0
- package/dist/tools/path-traversal.test.js.map +1 -0
- package/dist/tools/pick-best-takes.js +2 -1
- package/dist/tools/pick-best-takes.js.map +1 -1
- package/dist/tools/punch-in.d.ts.map +1 -1
- package/dist/tools/punch-in.js +2 -1
- package/dist/tools/punch-in.js.map +1 -1
- package/dist/tools/read-transcript.js +2 -1
- package/dist/tools/read-transcript.js.map +1 -1
- package/dist/tools/render-multi-format.d.ts +35 -0
- package/dist/tools/render-multi-format.d.ts.map +1 -0
- package/dist/tools/render-multi-format.js +206 -0
- package/dist/tools/render-multi-format.js.map +1 -0
- package/dist/tools/render-multi-format.test.d.ts +2 -0
- package/dist/tools/render-multi-format.test.d.ts.map +1 -0
- package/dist/tools/render-multi-format.test.js +312 -0
- package/dist/tools/render-multi-format.test.js.map +1 -0
- package/dist/tools/render.d.ts.map +1 -1
- package/dist/tools/render.js +2 -2
- package/dist/tools/render.js.map +1 -1
- package/dist/tools/rewrite-hook.d.ts +32 -0
- package/dist/tools/rewrite-hook.d.ts.map +1 -0
- package/dist/tools/rewrite-hook.js +65 -0
- package/dist/tools/rewrite-hook.js.map +1 -0
- package/dist/tools/score-clip.d.ts +30 -0
- package/dist/tools/score-clip.d.ts.map +1 -0
- package/dist/tools/score-clip.js +109 -0
- package/dist/tools/score-clip.js.map +1 -0
- package/dist/tools/score-clip.test.d.ts +2 -0
- package/dist/tools/score-clip.test.d.ts.map +1 -0
- package/dist/tools/score-clip.test.js +110 -0
- package/dist/tools/score-clip.test.js.map +1 -0
- package/dist/tools/search-tools.d.ts +34 -0
- package/dist/tools/search-tools.d.ts.map +1 -0
- package/dist/tools/search-tools.js +86 -0
- package/dist/tools/search-tools.js.map +1 -0
- package/dist/tools/search-tools.test.d.ts +2 -0
- package/dist/tools/search-tools.test.d.ts.map +1 -0
- package/dist/tools/search-tools.test.js +60 -0
- package/dist/tools/search-tools.test.js.map +1 -0
- package/dist/tools/snap-cuts-to-beats.d.ts +18 -0
- package/dist/tools/snap-cuts-to-beats.d.ts.map +1 -0
- package/dist/tools/snap-cuts-to-beats.js +110 -0
- package/dist/tools/snap-cuts-to-beats.js.map +1 -0
- package/dist/tools/snap-cuts-to-beats.test.d.ts +2 -0
- package/dist/tools/snap-cuts-to-beats.test.d.ts.map +1 -0
- package/dist/tools/snap-cuts-to-beats.test.js +99 -0
- package/dist/tools/snap-cuts-to-beats.test.js.map +1 -0
- package/dist/tools/speed-ramp.d.ts.map +1 -1
- package/dist/tools/speed-ramp.js +2 -1
- package/dist/tools/speed-ramp.js.map +1 -1
- package/dist/tools/stabilize-video.d.ts.map +1 -1
- package/dist/tools/stabilize-video.js +2 -1
- package/dist/tools/stabilize-video.js.map +1 -1
- package/dist/tools/suggest-broll.d.ts +34 -0
- package/dist/tools/suggest-broll.d.ts.map +1 -0
- package/dist/tools/suggest-broll.js +367 -0
- package/dist/tools/suggest-broll.js.map +1 -0
- package/dist/tools/suggest-broll.test.d.ts +2 -0
- package/dist/tools/suggest-broll.test.d.ts.map +1 -0
- package/dist/tools/suggest-broll.test.js +217 -0
- package/dist/tools/suggest-broll.test.js.map +1 -0
- package/dist/tools/text-based-cut.d.ts +33 -0
- package/dist/tools/text-based-cut.d.ts.map +1 -0
- package/dist/tools/text-based-cut.js +172 -0
- package/dist/tools/text-based-cut.js.map +1 -0
- package/dist/tools/text-based-cut.test.d.ts +2 -0
- package/dist/tools/text-based-cut.test.d.ts.map +1 -0
- package/dist/tools/text-based-cut.test.js +32 -0
- package/dist/tools/text-based-cut.test.js.map +1 -0
- package/dist/tools/transcribe.d.ts +1 -1
- package/dist/tools/transition-videos.d.ts +1 -1
- package/dist/tools/transition-videos.d.ts.map +1 -1
- package/dist/tools/transition-videos.js +2 -1
- package/dist/tools/transition-videos.js.map +1 -1
- package/dist/tools/trim-dead-air.d.ts +59 -0
- package/dist/tools/trim-dead-air.d.ts.map +1 -0
- package/dist/tools/trim-dead-air.js +215 -0
- package/dist/tools/trim-dead-air.js.map +1 -0
- package/dist/tools/trim-dead-air.test.d.ts +2 -0
- package/dist/tools/trim-dead-air.test.d.ts.map +1 -0
- package/dist/tools/trim-dead-air.test.js +75 -0
- package/dist/tools/trim-dead-air.test.js.map +1 -0
- package/dist/tools/verify-thumbnail-promise.d.ts +33 -0
- package/dist/tools/verify-thumbnail-promise.d.ts.map +1 -0
- package/dist/tools/verify-thumbnail-promise.js +112 -0
- package/dist/tools/verify-thumbnail-promise.js.map +1 -0
- package/dist/tools/verify-thumbnail-promise.test.d.ts +2 -0
- package/dist/tools/verify-thumbnail-promise.test.d.ts.map +1 -0
- package/dist/tools/verify-thumbnail-promise.test.js +38 -0
- package/dist/tools/verify-thumbnail-promise.test.js.map +1 -0
- package/dist/tools/write-keyword-captions.d.ts +7 -0
- package/dist/tools/write-keyword-captions.d.ts.map +1 -1
- package/dist/tools/write-keyword-captions.js +35 -4
- package/dist/tools/write-keyword-captions.js.map +1 -1
- package/dist/ui/App.d.ts.map +1 -1
- package/dist/ui/App.js +75 -11
- package/dist/ui/App.js.map +1 -1
- package/dist/ui/tool-formatters.d.ts +30 -0
- package/dist/ui/tool-formatters.d.ts.map +1 -0
- package/dist/ui/tool-formatters.js +461 -0
- package/dist/ui/tool-formatters.js.map +1 -0
- package/dist/ui/tool-formatters.test.d.ts +2 -0
- package/dist/ui/tool-formatters.test.d.ts.map +1 -0
- package/dist/ui/tool-formatters.test.js +143 -0
- package/dist/ui/tool-formatters.test.js.map +1 -0
- package/package.json +10 -9
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Clip scoring — does this arbitrary window earn the watch?
|
|
3
|
+
*
|
|
4
|
+
* `analyze_hook` only scores the first 3 seconds (the algorithmic
|
|
5
|
+
* checkpoint). Once you're past the hook, the next questions are:
|
|
6
|
+
*
|
|
7
|
+
* - Hook — does the OPENER (first ~5s) earn the watch?
|
|
8
|
+
* - Flow — does the middle stay engaging without dragging?
|
|
9
|
+
* - Engagement — emotional intensity, curiosity gap, payoff.
|
|
10
|
+
* - Trend — is the topic something currently shareable?
|
|
11
|
+
*
|
|
12
|
+
* One LLM call: transcript window in (and optional sample frames),
|
|
13
|
+
* structured 0-1 scores out, weighted total 0-100. Pure-ish — the
|
|
14
|
+
* scoring rubric, prompt template, parser and weighting all live here so
|
|
15
|
+
* `find_viral_moments` can reuse the engine without going through the
|
|
16
|
+
* tool wrapper.
|
|
17
|
+
*/
|
|
18
|
+
import { readFileSync } from "node:fs";
|
|
19
|
+
import { resolveApiKey } from "./auth/api-keys.js";
|
|
20
|
+
const DEFAULT_WEIGHTS = {
|
|
21
|
+
hook: 30,
|
|
22
|
+
flow: 25,
|
|
23
|
+
engagement: 30,
|
|
24
|
+
trend: 15,
|
|
25
|
+
};
|
|
26
|
+
const CLIP_SYSTEM = `You are a short-form video selector. Your job is to score a CLIP (transcript ± sample frames) on its potential to go viral as a standalone Short / Reel / TikTok.
|
|
27
|
+
|
|
28
|
+
Output a JSON object with these EXACT keys (no extras, no prose):
|
|
29
|
+
{
|
|
30
|
+
"hook": <0..1>,
|
|
31
|
+
"flow": <0..1>,
|
|
32
|
+
"engagement": <0..1>,
|
|
33
|
+
"trend": <0..1>,
|
|
34
|
+
"why": "<≤160 char rationale: name the strongest + weakest dimension>"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
Scoring rubric:
|
|
38
|
+
|
|
39
|
+
hook — Does the OPENING line earn the watch?
|
|
40
|
+
1.0 = strong scroll-stop opener (question, claim, shocking number, named subject);
|
|
41
|
+
0.5 = neutral lead; 0.0 = throat-clear / "so basically..." / dead air.
|
|
42
|
+
|
|
43
|
+
flow — Does the clip stay engaging across its full duration?
|
|
44
|
+
1.0 = every beat earns its time, no dragging, payoff lands;
|
|
45
|
+
0.5 = some slack but recovers; 0.0 = meanders / repeats / loses thread.
|
|
46
|
+
|
|
47
|
+
engagement — Emotional intensity, curiosity gap, surprise, payoff.
|
|
48
|
+
1.0 = strong emotional beat (laugh / shock / tension / aha);
|
|
49
|
+
0.5 = mildly interesting; 0.0 = flat informational.
|
|
50
|
+
|
|
51
|
+
trend — Topic currency. Does it intersect with what's currently
|
|
52
|
+
trending / evergreen-shareable on short-form?
|
|
53
|
+
1.0 = on-trend (AI, money, dating, fitness, productivity, gaming meta, current events);
|
|
54
|
+
0.5 = niche but proven shareable; 0.0 = obscure / dated / business-jargon.
|
|
55
|
+
|
|
56
|
+
Be honest. A technically-clean but flat clip should score 0.3-0.4, not 0.6+.`;
|
|
57
|
+
/**
|
|
58
|
+
* Robust parser — tolerates prose around the JSON, missing keys, and
|
|
59
|
+
* out-of-range numbers. Mirrors `parseHookVisionResponse`.
|
|
60
|
+
*/
|
|
61
|
+
export function parseClipScoreResponse(content) {
|
|
62
|
+
const start = content.indexOf("{");
|
|
63
|
+
const end = content.lastIndexOf("}");
|
|
64
|
+
if (start < 0 || end < start) {
|
|
65
|
+
throw new Error("clip-scoring: no JSON object in response");
|
|
66
|
+
}
|
|
67
|
+
const parsed = JSON.parse(content.slice(start, end + 1));
|
|
68
|
+
return {
|
|
69
|
+
hook: clamp01(num(parsed.hook, 0)),
|
|
70
|
+
flow: clamp01(num(parsed.flow, 0)),
|
|
71
|
+
engagement: clamp01(num(parsed.engagement, 0)),
|
|
72
|
+
trend: clamp01(num(parsed.trend, 0)),
|
|
73
|
+
why: String(parsed.why ?? "").slice(0, 240),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Combine the four 0-1 dimensions into a single 0-100 score, using the
|
|
78
|
+
* default (or caller-supplied) weights.
|
|
79
|
+
*/
|
|
80
|
+
export function combineClipScore(vision, durationSec, weights = {}) {
|
|
81
|
+
const w = { ...DEFAULT_WEIGHTS, ...weights };
|
|
82
|
+
const wSum = w.hook + w.flow + w.engagement + w.trend;
|
|
83
|
+
const raw = w.hook * vision.hook +
|
|
84
|
+
w.flow * vision.flow +
|
|
85
|
+
w.engagement * vision.engagement +
|
|
86
|
+
w.trend * vision.trend;
|
|
87
|
+
const score = Math.round((raw / wSum) * 100);
|
|
88
|
+
return {
|
|
89
|
+
score,
|
|
90
|
+
hook: vision.hook,
|
|
91
|
+
flow: vision.flow,
|
|
92
|
+
engagement: vision.engagement,
|
|
93
|
+
trend: vision.trend,
|
|
94
|
+
why: vision.why,
|
|
95
|
+
durationSec,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Run the LLM scoring pass. Frames are optional — transcript-only is
|
|
100
|
+
* the cheap default; pass frames when visual energy matters.
|
|
101
|
+
*
|
|
102
|
+
* Pure-ish: the only side effects are the OpenAI fetch and reading
|
|
103
|
+
* frame files off disk. Used directly by both the tool wrapper and
|
|
104
|
+
* `find_viral_moments`.
|
|
105
|
+
*/
|
|
106
|
+
export async function scoreClipInternal(transcriptText, startSec, endSec, framePaths = [], opts = {}) {
|
|
107
|
+
const apiKey = opts.apiKey ?? resolveApiKey("OPENAI_API_KEY", "openai");
|
|
108
|
+
if (!apiKey)
|
|
109
|
+
throw new Error("OPENAI_API_KEY required for score_clip.");
|
|
110
|
+
const durationSec = Math.max(0, endSec - startSec);
|
|
111
|
+
const detail = opts.detail ?? "low";
|
|
112
|
+
const model = opts.model ?? "gpt-4o-mini";
|
|
113
|
+
const userContent = [
|
|
114
|
+
{
|
|
115
|
+
type: "text",
|
|
116
|
+
text: `CLIP WINDOW: ${startSec.toFixed(2)}s → ${endSec.toFixed(2)}s ` +
|
|
117
|
+
`(${durationSec.toFixed(2)}s).\n\nTRANSCRIPT:\n${transcriptText.trim() || "(empty)"}`,
|
|
118
|
+
},
|
|
119
|
+
];
|
|
120
|
+
for (const p of framePaths) {
|
|
121
|
+
const mime = p.toLowerCase().endsWith(".png") ? "image/png" : "image/jpeg";
|
|
122
|
+
const b64 = readFileSync(p).toString("base64");
|
|
123
|
+
userContent.push({
|
|
124
|
+
type: "image_url",
|
|
125
|
+
image_url: { url: `data:${mime};base64,${b64}`, detail },
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const body = {
|
|
129
|
+
model,
|
|
130
|
+
messages: [
|
|
131
|
+
{ role: "system", content: CLIP_SYSTEM },
|
|
132
|
+
{ role: "user", content: userContent },
|
|
133
|
+
],
|
|
134
|
+
response_format: { type: "json_object" },
|
|
135
|
+
temperature: 0,
|
|
136
|
+
};
|
|
137
|
+
const res = await fetch("https://api.openai.com/v1/chat/completions", {
|
|
138
|
+
method: "POST",
|
|
139
|
+
headers: {
|
|
140
|
+
Authorization: `Bearer ${apiKey}`,
|
|
141
|
+
"Content-Type": "application/json",
|
|
142
|
+
},
|
|
143
|
+
body: JSON.stringify(body),
|
|
144
|
+
signal: opts.signal,
|
|
145
|
+
});
|
|
146
|
+
if (!res.ok) {
|
|
147
|
+
const text = await res.text().catch(() => "");
|
|
148
|
+
throw new Error(`OpenAI HTTP ${res.status}: ${text.slice(0, 200)}`);
|
|
149
|
+
}
|
|
150
|
+
const data = (await res.json());
|
|
151
|
+
const content = data.choices[0]?.message?.content;
|
|
152
|
+
if (!content)
|
|
153
|
+
throw new Error("score_clip: empty model response");
|
|
154
|
+
const parsed = parseClipScoreResponse(content);
|
|
155
|
+
return combineClipScore(parsed, durationSec, opts.weights);
|
|
156
|
+
}
|
|
157
|
+
// ── Helpers ─────────────────────────────────────────────────
|
|
158
|
+
function num(v, fb) {
|
|
159
|
+
const n = typeof v === "number" ? v : Number(v);
|
|
160
|
+
return Number.isFinite(n) ? n : fb;
|
|
161
|
+
}
|
|
162
|
+
function clamp01(n) {
|
|
163
|
+
return Math.max(0, Math.min(1, n));
|
|
164
|
+
}
|
|
165
|
+
//# sourceMappingURL=clip-scoring.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clip-scoring.js","sourceRoot":"","sources":["../../src/core/clip-scoring.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clip-scoring.test.d.ts","sourceRoot":"","sources":["../../src/core/clip-scoring.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { combineClipScore, parseClipScoreResponse, scoreClipInternal } from "./clip-scoring.js";
|
|
3
|
+
describe("parseClipScoreResponse", () => {
|
|
4
|
+
it("parses a well-formed response", () => {
|
|
5
|
+
const r = parseClipScoreResponse(JSON.stringify({ hook: 0.8, flow: 0.7, engagement: 0.9, trend: 0.4, why: "strong open" }));
|
|
6
|
+
expect(r.hook).toBe(0.8);
|
|
7
|
+
expect(r.flow).toBe(0.7);
|
|
8
|
+
expect(r.engagement).toBe(0.9);
|
|
9
|
+
expect(r.trend).toBe(0.4);
|
|
10
|
+
expect(r.why).toContain("strong");
|
|
11
|
+
});
|
|
12
|
+
it("clamps out-of-range values to [0,1]", () => {
|
|
13
|
+
const r = parseClipScoreResponse(JSON.stringify({ hook: 99, flow: -5, engagement: 0.5, trend: 1 }));
|
|
14
|
+
expect(r.hook).toBe(1);
|
|
15
|
+
expect(r.flow).toBe(0);
|
|
16
|
+
expect(r.engagement).toBe(0.5);
|
|
17
|
+
expect(r.trend).toBe(1);
|
|
18
|
+
});
|
|
19
|
+
it("falls back to 0 on missing fields", () => {
|
|
20
|
+
const r = parseClipScoreResponse("{}");
|
|
21
|
+
expect(r.hook).toBe(0);
|
|
22
|
+
expect(r.flow).toBe(0);
|
|
23
|
+
expect(r.engagement).toBe(0);
|
|
24
|
+
expect(r.trend).toBe(0);
|
|
25
|
+
});
|
|
26
|
+
it("survives prose wrapping the JSON", () => {
|
|
27
|
+
const r = parseClipScoreResponse('pre {"hook":0.5,"flow":0.5,"engagement":0.5,"trend":0.5,"why":"x"} post');
|
|
28
|
+
expect(r.hook).toBe(0.5);
|
|
29
|
+
});
|
|
30
|
+
it("throws on totally absent JSON", () => {
|
|
31
|
+
expect(() => parseClipScoreResponse("no json here")).toThrow(/no JSON/);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("combineClipScore", () => {
|
|
35
|
+
it("produces 100 for max scores with default weights", () => {
|
|
36
|
+
const r = combineClipScore({ hook: 1, flow: 1, engagement: 1, trend: 1, why: "" }, 30);
|
|
37
|
+
expect(r.score).toBe(100);
|
|
38
|
+
expect(r.durationSec).toBe(30);
|
|
39
|
+
});
|
|
40
|
+
it("produces 0 for min scores", () => {
|
|
41
|
+
const r = combineClipScore({ hook: 0, flow: 0, engagement: 0, trend: 0, why: "" }, 30);
|
|
42
|
+
expect(r.score).toBe(0);
|
|
43
|
+
});
|
|
44
|
+
it("respects custom weights", () => {
|
|
45
|
+
// hook=1, others=0; with weights {hook:100, flow:0, engagement:0, trend:0}
|
|
46
|
+
// → 100. Default weights would put it at 30.
|
|
47
|
+
const r = combineClipScore({ hook: 1, flow: 0, engagement: 0, trend: 0, why: "" }, 30, {
|
|
48
|
+
hook: 100,
|
|
49
|
+
flow: 0,
|
|
50
|
+
engagement: 0,
|
|
51
|
+
trend: 0,
|
|
52
|
+
});
|
|
53
|
+
expect(r.score).toBe(100);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
describe("scoreClipInternal", () => {
|
|
57
|
+
afterEach(() => {
|
|
58
|
+
vi.unstubAllGlobals();
|
|
59
|
+
delete process.env.OPENAI_API_KEY;
|
|
60
|
+
});
|
|
61
|
+
it("throws when no API key is configured", async () => {
|
|
62
|
+
delete process.env.OPENAI_API_KEY;
|
|
63
|
+
await expect(scoreClipInternal("hello world", 0, 10)).rejects.toThrow(/OPENAI_API_KEY/);
|
|
64
|
+
});
|
|
65
|
+
it("calls OpenAI chat completions with json_object response format and parses the result", async () => {
|
|
66
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
67
|
+
let captured;
|
|
68
|
+
vi.stubGlobal("fetch", vi.fn(async (url, init) => {
|
|
69
|
+
captured = { url, body: JSON.parse(init?.body ?? "{}") };
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
status: 200,
|
|
73
|
+
json: async () => ({
|
|
74
|
+
choices: [
|
|
75
|
+
{
|
|
76
|
+
message: {
|
|
77
|
+
content: JSON.stringify({
|
|
78
|
+
hook: 0.8,
|
|
79
|
+
flow: 0.6,
|
|
80
|
+
engagement: 0.7,
|
|
81
|
+
trend: 0.5,
|
|
82
|
+
why: "tight open, payoff lands",
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}),
|
|
88
|
+
text: async () => "",
|
|
89
|
+
};
|
|
90
|
+
}));
|
|
91
|
+
const r = await scoreClipInternal("a strong hook line", 5, 25);
|
|
92
|
+
expect(r.score).toBeGreaterThan(0);
|
|
93
|
+
expect(r.score).toBeLessThanOrEqual(100);
|
|
94
|
+
expect(r.hook).toBe(0.8);
|
|
95
|
+
expect(r.durationSec).toBe(20);
|
|
96
|
+
expect(captured?.url).toBe("https://api.openai.com/v1/chat/completions");
|
|
97
|
+
expect(captured?.body.response_format).toEqual({ type: "json_object" });
|
|
98
|
+
expect(captured?.body.temperature).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
it("propagates HTTP errors with the OpenAI status", async () => {
|
|
101
|
+
process.env.OPENAI_API_KEY = "test-key";
|
|
102
|
+
vi.stubGlobal("fetch", vi.fn(async () => {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
status: 429,
|
|
106
|
+
text: async () => "rate limited",
|
|
107
|
+
json: async () => ({}),
|
|
108
|
+
};
|
|
109
|
+
}));
|
|
110
|
+
await expect(scoreClipInternal("hi", 0, 10)).rejects.toThrow(/429/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=clip-scoring.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"clip-scoring.test.js","sourceRoot":"","sources":["../../src/core/clip-scoring.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Emoji-injection pass for keyword captions.
|
|
3
|
+
*
|
|
4
|
+
* Submagic / CapCut have made the "💯 caption + 🔥 emoji" look the
|
|
5
|
+
* default vibe of viral short-form. Rather than ship a static emoji
|
|
6
|
+
* map, we ask the LLM (one call over ALL cues) for the single
|
|
7
|
+
* most-fitting emoji per cue (or empty when nothing clearly fits).
|
|
8
|
+
*
|
|
9
|
+
* Pure-ish: takes the OpenAI-style chat client as a dependency so the
|
|
10
|
+
* caller can swap in `fetch` directly OR mock it in tests.
|
|
11
|
+
*/
|
|
12
|
+
import type { AssCue } from "./ass.js";
|
|
13
|
+
export type EmojiDensity = "low" | "med" | "high";
|
|
14
|
+
export interface InjectEmojisOptions {
|
|
15
|
+
density?: EmojiDensity;
|
|
16
|
+
model?: string;
|
|
17
|
+
apiKey?: string;
|
|
18
|
+
signal?: AbortSignal;
|
|
19
|
+
/** Override fetch — for tests. Defaults to global fetch. */
|
|
20
|
+
fetchImpl?: typeof fetch;
|
|
21
|
+
}
|
|
22
|
+
export interface InjectEmojisResult {
|
|
23
|
+
cues: AssCue[];
|
|
24
|
+
/** Count of cues that received an emoji (excluding empty strings). */
|
|
25
|
+
injected: number;
|
|
26
|
+
/** When set, the LLM call failed and cues were returned unchanged. */
|
|
27
|
+
error?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Inject emojis into a list of cues via one LLM call. On failure
|
|
31
|
+
* (network down, malformed response, length mismatch) we return the
|
|
32
|
+
* cues UNCHANGED + populate `error` so the caller can surface it.
|
|
33
|
+
*
|
|
34
|
+
* For density="high" the emoji is rendered on its own line ABOVE the
|
|
35
|
+
* caption text (separated by ASS's `\\N` break) for the stacked CapCut
|
|
36
|
+
* look. For low/med it's appended inline.
|
|
37
|
+
*/
|
|
38
|
+
export declare function injectEmojis(cues: AssCue[], opts?: InjectEmojisOptions): Promise<InjectEmojisResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Tolerant parser. Accepts either {emojis: [...]} or a bare array, pads
|
|
41
|
+
* / truncates to `expected` length so length always lines up. Exported
|
|
42
|
+
* for testing.
|
|
43
|
+
*/
|
|
44
|
+
export declare function parseEmojiResponse(content: string, expected: number): string[];
|
|
45
|
+
//# sourceMappingURL=emoji-captions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji-captions.d.ts","sourceRoot":"","sources":["../../src/core/emoji-captions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAGvC,MAAM,MAAM,YAAY,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAElD,MAAM,WAAW,mBAAmB;IAClC,OAAO,CAAC,EAAE,YAAY,CAAC;IACvB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,4DAA4D;IAC5D,SAAS,CAAC,EAAE,OAAO,KAAK,CAAC;CAC1B;AAED,MAAM,WAAW,kBAAkB;IACjC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,sEAAsE;IACtE,QAAQ,EAAE,MAAM,CAAC;IACjB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAgBD;;;;;;;;GAQG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,GAAE,mBAAwB,GAC7B,OAAO,CAAC,kBAAkB,CAAC,CAgE7B;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,EAAE,CAsB9E"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { resolveApiKey } from "./auth/api-keys.js";
|
|
2
|
+
const SYSTEM = `You are an emoji selector for short-form video captions. For each caption line, return the SINGLE most-fitting emoji or an empty string when nothing clearly fits.
|
|
3
|
+
|
|
4
|
+
Rules:
|
|
5
|
+
- Output JSON: {"emojis": [string, ...]} aligned 1-to-1 with the input order. Length MUST equal the input length.
|
|
6
|
+
- Each entry is a single emoji character (e.g. "🔥") or "". No multi-emoji entries, no text.
|
|
7
|
+
- Don't force emojis where they'd feel random — empty string is the right answer ~half the time.
|
|
8
|
+
- Match the density target the user specifies.`;
|
|
9
|
+
const DENSITY_TARGETS = {
|
|
10
|
+
low: "Roughly 1 in 4 cues should get an emoji. Be very selective — only when an emoji genuinely amplifies the line.",
|
|
11
|
+
med: "Roughly 1 in 2 cues should get an emoji. Pick the obvious wins; leave neutral lines empty.",
|
|
12
|
+
high: "Every cue should get an emoji where ANY emoji fits. Empty only for purely connective lines.",
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Inject emojis into a list of cues via one LLM call. On failure
|
|
16
|
+
* (network down, malformed response, length mismatch) we return the
|
|
17
|
+
* cues UNCHANGED + populate `error` so the caller can surface it.
|
|
18
|
+
*
|
|
19
|
+
* For density="high" the emoji is rendered on its own line ABOVE the
|
|
20
|
+
* caption text (separated by ASS's `\\N` break) for the stacked CapCut
|
|
21
|
+
* look. For low/med it's appended inline.
|
|
22
|
+
*/
|
|
23
|
+
export async function injectEmojis(cues, opts = {}) {
|
|
24
|
+
if (cues.length === 0)
|
|
25
|
+
return { cues, injected: 0 };
|
|
26
|
+
const density = opts.density ?? "med";
|
|
27
|
+
const model = opts.model ?? "gpt-4o-mini";
|
|
28
|
+
const apiKey = opts.apiKey ?? resolveApiKey("OPENAI_API_KEY", "openai");
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
return { cues, injected: 0, error: "OPENAI_API_KEY not set" };
|
|
31
|
+
}
|
|
32
|
+
const numbered = cues.map((c, i) => `${i + 1}. ${c.text}`).join("\n");
|
|
33
|
+
const userText = `Density target: ${DENSITY_TARGETS[density]}\n\n` +
|
|
34
|
+
`Return exactly ${cues.length} entries.\n\n` +
|
|
35
|
+
`CAPTIONS:\n${numbered}`;
|
|
36
|
+
const fetchImpl = opts.fetchImpl ?? fetch;
|
|
37
|
+
let emojis;
|
|
38
|
+
try {
|
|
39
|
+
const res = await fetchImpl("https://api.openai.com/v1/chat/completions", {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
Authorization: `Bearer ${apiKey}`,
|
|
43
|
+
"Content-Type": "application/json",
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
model,
|
|
47
|
+
messages: [
|
|
48
|
+
{ role: "system", content: SYSTEM },
|
|
49
|
+
{ role: "user", content: userText },
|
|
50
|
+
],
|
|
51
|
+
response_format: { type: "json_object" },
|
|
52
|
+
temperature: 0,
|
|
53
|
+
}),
|
|
54
|
+
signal: opts.signal,
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const text = await res.text().catch(() => "");
|
|
58
|
+
return {
|
|
59
|
+
cues,
|
|
60
|
+
injected: 0,
|
|
61
|
+
error: `OpenAI HTTP ${res.status}: ${text.slice(0, 160)}`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const data = (await res.json());
|
|
65
|
+
const content = data.choices?.[0]?.message?.content ?? "";
|
|
66
|
+
emojis = parseEmojiResponse(content, cues.length);
|
|
67
|
+
}
|
|
68
|
+
catch (e) {
|
|
69
|
+
return { cues, injected: 0, error: e.message };
|
|
70
|
+
}
|
|
71
|
+
if (emojis.length !== cues.length) {
|
|
72
|
+
return { cues, injected: 0, error: "emoji count mismatch" };
|
|
73
|
+
}
|
|
74
|
+
let injected = 0;
|
|
75
|
+
const out = cues.map((c, i) => {
|
|
76
|
+
const e = (emojis[i] ?? "").trim();
|
|
77
|
+
if (!e)
|
|
78
|
+
return c;
|
|
79
|
+
injected++;
|
|
80
|
+
const text = density === "high" ? `${e}\\N${c.text}` : `${c.text} ${e}`;
|
|
81
|
+
return { ...c, text };
|
|
82
|
+
});
|
|
83
|
+
return { cues: out, injected };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Tolerant parser. Accepts either {emojis: [...]} or a bare array, pads
|
|
87
|
+
* / truncates to `expected` length so length always lines up. Exported
|
|
88
|
+
* for testing.
|
|
89
|
+
*/
|
|
90
|
+
export function parseEmojiResponse(content, expected) {
|
|
91
|
+
let arr;
|
|
92
|
+
try {
|
|
93
|
+
const obj = JSON.parse(content);
|
|
94
|
+
if (Array.isArray(obj))
|
|
95
|
+
arr = obj;
|
|
96
|
+
else if (typeof obj === "object" && obj !== null) {
|
|
97
|
+
const v = obj.emojis;
|
|
98
|
+
arr = Array.isArray(v) ? v : [];
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
arr = [];
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
const m = content.match(/\[[\s\S]*\]/);
|
|
105
|
+
if (m) {
|
|
106
|
+
try {
|
|
107
|
+
arr = JSON.parse(m[0]);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
arr = [];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
else
|
|
114
|
+
arr = [];
|
|
115
|
+
}
|
|
116
|
+
const list = Array.isArray(arr) ? arr.map((v) => (typeof v === "string" ? v : "")) : [];
|
|
117
|
+
while (list.length < expected)
|
|
118
|
+
list.push("");
|
|
119
|
+
return list.slice(0, expected);
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=emoji-captions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"emoji-captions.js","sourceRoot":"","sources":["../../src/core/emoji-captions.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Face-tracked reframe — pure planning + ffmpeg-filter synthesis.
|
|
3
|
+
*
|
|
4
|
+
* `analyzeReframe` shells out to the MediaPipe / PySceneDetect sidecar to get
|
|
5
|
+
* a per-shot smoothed face centre. `buildReframeFilter` is a pure function
|
|
6
|
+
* that compiles the plan into a single ffmpeg `crop=...` expression that
|
|
7
|
+
* follows the subject across shots. Splitting the heavy I/O from the
|
|
8
|
+
* filter math keeps the latter unit-testable without Python installed.
|
|
9
|
+
*/
|
|
10
|
+
export type Aspect = "9:16" | "1:1" | "4:5" | "16:9";
|
|
11
|
+
export interface AspectRatio {
|
|
12
|
+
/** Width side of the ratio (e.g. 9 for 9:16). */
|
|
13
|
+
w: number;
|
|
14
|
+
/** Height side of the ratio (e.g. 16 for 9:16). */
|
|
15
|
+
h: number;
|
|
16
|
+
}
|
|
17
|
+
export declare function parseAspect(aspect: Aspect): AspectRatio;
|
|
18
|
+
export interface ReframeFrameSample {
|
|
19
|
+
atSec: number;
|
|
20
|
+
faceCx: number;
|
|
21
|
+
faceCy: number;
|
|
22
|
+
faceW: number;
|
|
23
|
+
faceH: number;
|
|
24
|
+
}
|
|
25
|
+
export type ShotMode = "face" | "motion" | "static";
|
|
26
|
+
export interface ReframeShot {
|
|
27
|
+
startSec: number;
|
|
28
|
+
endSec: number;
|
|
29
|
+
frames: ReframeFrameSample[];
|
|
30
|
+
/** Smoothed centre X (normalized 0-1). */
|
|
31
|
+
smoothedX: number;
|
|
32
|
+
/** Smoothed centre Y (normalized 0-1). */
|
|
33
|
+
smoothedY: number;
|
|
34
|
+
mode: ShotMode;
|
|
35
|
+
}
|
|
36
|
+
export interface ReframePlan {
|
|
37
|
+
shots: ReframeShot[];
|
|
38
|
+
totalSec: number;
|
|
39
|
+
fps: number;
|
|
40
|
+
sourceWidth: number;
|
|
41
|
+
sourceHeight: number;
|
|
42
|
+
}
|
|
43
|
+
export interface AnalyzeReframeOptions {
|
|
44
|
+
signal?: AbortSignal;
|
|
45
|
+
/** Frames per second to face-detect. Default 5. */
|
|
46
|
+
sampleFps?: number;
|
|
47
|
+
/** MediaPipe min_detection_confidence. Default 0.5. */
|
|
48
|
+
minDetectionConfidence?: number;
|
|
49
|
+
/** Smoothing window in seconds (informational; sidecar uses median). Default 0.5. */
|
|
50
|
+
smoothingWindowSec?: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Spawn the face-reframe sidecar; return the parsed shot plan.
|
|
54
|
+
* Throws on sidecar failure with the structured error embedded.
|
|
55
|
+
*/
|
|
56
|
+
export declare function analyzeReframe(videoPath: string, opts?: AnalyzeReframeOptions): Promise<ReframePlan>;
|
|
57
|
+
export interface BuildReframeFilterResult {
|
|
58
|
+
/** ffmpeg `-vf` value (already includes both crop + scale). */
|
|
59
|
+
filter: string;
|
|
60
|
+
/** Output width (after crop, before scale-back). */
|
|
61
|
+
outWidth: number;
|
|
62
|
+
/** Output height (after crop, before scale-back). */
|
|
63
|
+
outHeight: number;
|
|
64
|
+
/** Per-shot crop X positions (after clamping into [0, srcW-outW]). */
|
|
65
|
+
shotXs: number[];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Compute the crop dimensions for a target aspect when cropping FROM a
|
|
69
|
+
* source frame. The crop is sized to fit ENTIRELY inside the source — i.e.
|
|
70
|
+
* we letterbox neither dimension. The returned `outW` × `outH` matches the
|
|
71
|
+
* target aspect exactly.
|
|
72
|
+
*
|
|
73
|
+
* Pure / unit-testable.
|
|
74
|
+
*/
|
|
75
|
+
export declare function reframeCropSize(srcW: number, srcH: number, aspect: Aspect): {
|
|
76
|
+
outW: number;
|
|
77
|
+
outH: number;
|
|
78
|
+
};
|
|
79
|
+
/**
|
|
80
|
+
* Compile a per-shot smoothed-face plan into one ffmpeg `crop` expression
|
|
81
|
+
* that switches X position by timestamp. Each shot contributes one
|
|
82
|
+
* `if(between(t, a, b), x_i, ...)` tier; the outermost ELSE is a centre
|
|
83
|
+
* crop so the filter is a no-op outside any defined shot.
|
|
84
|
+
*
|
|
85
|
+
* The crop's Y is left static (we don't currently track vertical motion;
|
|
86
|
+
* faces tend to dominate the upper third on portrait crops anyway).
|
|
87
|
+
*
|
|
88
|
+
* Pure / unit-testable. No ffmpeg execution.
|
|
89
|
+
*/
|
|
90
|
+
export declare function buildReframeFilter(plan: ReframePlan, aspect: Aspect): BuildReframeFilterResult;
|
|
91
|
+
//# sourceMappingURL=face-reframe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"face-reframe.d.ts","sourceRoot":"","sources":["../../src/core/face-reframe.ts"],"names":[],"mappings":"AAGA;;;;;;;;GAQG;AAEH,MAAM,MAAM,MAAM,GAAG,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,MAAM,CAAC;AAErD,MAAM,WAAW,WAAW;IAC1B,iDAAiD;IACjD,CAAC,EAAE,MAAM,CAAC;IACV,mDAAmD;IACnD,CAAC,EAAE,MAAM,CAAC;CACX;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,MAAM,GAAG,WAAW,CAMvD;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEpD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,kBAAkB,EAAE,CAAC;IAC7B,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,0CAA0C;IAC1C,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,QAAQ,CAAC;CAChB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,WAAW,EAAE,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,GAAG,EAAE,MAAM,CAAC;IACZ,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,mDAAmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qFAAqF;IACrF,kBAAkB,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAClC,SAAS,EAAE,MAAM,EACjB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,WAAW,CAAC,CAiCtB;AAED,MAAM,WAAW,wBAAwB;IACvC,+DAA+D;IAC/D,MAAM,EAAE,MAAM,CAAC;IACf,oDAAoD;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,qDAAqD;IACrD,SAAS,EAAE,MAAM,CAAC;IAClB,sEAAsE;IACtE,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,GACb;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CA6BhC;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,WAAW,EAAE,MAAM,EAAE,MAAM,GAAG,wBAAwB,CAsC9F"}
|