videobook 0.0.0-stage → 0.2.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/CHANGELOG.md +54 -0
- package/LICENSE +21 -0
- package/README.md +195 -2
- package/bin/videobook.js +7 -0
- package/dist/acestep/client.js +203 -0
- package/dist/acestep/config.js +62 -0
- package/dist/acestep/errors.js +8 -0
- package/dist/acestep/process.js +175 -0
- package/dist/alive-captions/analyze.js +117 -0
- package/dist/alive-captions/audio.js +179 -0
- package/dist/alive-captions/frames.js +59 -0
- package/dist/alive-captions/model.js +37 -0
- package/dist/alive-captions/schema.js +49 -0
- package/dist/alive-captions/track.js +9 -0
- package/dist/api-key.js +33 -0
- package/dist/app-state.js +56 -0
- package/dist/app.js +257 -0
- package/dist/archive-b2.js +332 -0
- package/dist/archive-store.js +169 -0
- package/dist/artifact-label.js +36 -0
- package/dist/artifact-refs.js +49 -0
- package/dist/asset-tag-extraction.js +169 -0
- package/dist/asset-tags.js +228 -0
- package/dist/asset-usage.js +23 -0
- package/dist/audio-cue.js +71 -0
- package/dist/audio-encoding.js +32 -0
- package/dist/audio-sync-request.js +1 -0
- package/dist/audio-sync.js +223 -0
- package/dist/audio-trim-range.js +31 -0
- package/dist/book-context.js +31 -0
- package/dist/book-manager.js +374 -0
- package/dist/catalog-gc.js +26 -0
- package/dist/chain-executor.js +815 -0
- package/dist/chain-generation.js +115 -0
- package/dist/chain-plan.js +746 -0
- package/dist/chain-step-review.js +60 -0
- package/dist/character-name.js +25 -0
- package/dist/chat-generation-routing.js +73 -0
- package/dist/chat-generation-settings.js +78 -0
- package/dist/chat-models.js +44 -0
- package/dist/chat-prompt-rewrite.js +67 -0
- package/dist/chat-router.js +628 -0
- package/dist/chat-view-actions.js +49 -0
- package/dist/cli/args.js +242 -0
- package/dist/cli/config-command.js +103 -0
- package/dist/cli/doctor-command.js +289 -0
- package/dist/cli/entry.js +2 -0
- package/dist/cli/key-report.js +16 -0
- package/dist/cli/main.js +104 -0
- package/dist/cli/mcp-command.js +88 -0
- package/dist/cli/mcp-relay.js +51 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/run-command.js +231 -0
- package/dist/cli/serve-command.js +62 -0
- package/dist/cli/setup-command.js +60 -0
- package/dist/cli/stdio-guard.js +8 -0
- package/dist/cold-open-model.js +7 -0
- package/dist/cold-open.js +79 -0
- package/dist/command-reference.js +95 -0
- package/dist/content-store.js +74 -0
- package/dist/cost.js +8 -0
- package/dist/crop-geometry.js +23 -0
- package/dist/dancing-workflow.js +447 -0
- package/dist/data-home.js +44 -0
- package/dist/diagnostics.js +109 -0
- package/dist/dialog-quality.js +34 -0
- package/dist/downloads-dest.js +56 -0
- package/dist/editing-moments.js +194 -0
- package/dist/elevenlabs-audio-extension.js +66 -0
- package/dist/engine.js +54 -0
- package/dist/env-file.js +91 -0
- package/dist/env-secrets.js +148 -0
- package/dist/ffmpeg-binary.js +12 -0
- package/dist/ffmpeg.js +425 -0
- package/dist/ffprobe-binary.js +77 -0
- package/dist/forget-asset.js +216 -0
- package/dist/frame-utils.js +27 -0
- package/dist/generation-credit-error.js +11 -0
- package/dist/generation-input-mentions.js +117 -0
- package/dist/generation-input-policy.js +68 -0
- package/dist/generation-reference-mentions.js +97 -0
- package/dist/goal-characters.js +49 -0
- package/dist/goal-plan-v3.js +273 -0
- package/dist/goal-plan.js +220 -0
- package/dist/goal-planner-prompt.js +28 -0
- package/dist/goal-prompt-budget.js +4 -0
- package/dist/goal-reference-images.js +25 -0
- package/dist/goal-state.js +261 -0
- package/dist/goal-stills.js +153 -0
- package/dist/goal-store.js +144 -0
- package/dist/goal-styles.js +144 -0
- package/dist/goal-v3-runtime.js +157 -0
- package/dist/guides/character_schema_guide.md +70 -0
- package/dist/guides/gpt_image_2_5_prompting_guide.md +21 -0
- package/dist/guides/grok_imagine_prompting_guide.md +72 -0
- package/dist/guides/image_reproduction_guide.md +84 -0
- package/dist/guides/kling_3_0_prompting_guide.md +136 -0
- package/dist/guides/nano_banana_pro_prompting_guide.md +28 -0
- package/dist/guides/seedance_2_prompting_guide.md +172 -0
- package/dist/http-access.js +49 -0
- package/dist/http-byte-range.js +31 -0
- package/dist/hydration.js +56 -0
- package/dist/image-compress.js +77 -0
- package/dist/image-edit-default.js +1 -0
- package/dist/image-edit-models.js +41 -0
- package/dist/image-edit-settings.js +11 -0
- package/dist/image-region-edit.js +32 -0
- package/dist/image-subject-map.js +523 -0
- package/dist/image.js +46 -0
- package/dist/index.js +12 -0
- package/dist/job-deadline.js +60 -0
- package/dist/jobs/_alibaba-upload.js +30 -0
- package/dist/jobs/_audio-chunks.js +31 -0
- package/dist/jobs/_download-into-asset.js +15 -0
- package/dist/jobs/_fal-shared.js +22 -0
- package/dist/jobs/_fal-upload.js +35 -0
- package/dist/jobs/_image-edit-adapter.js +213 -0
- package/dist/jobs/_kie-poll.js +119 -0
- package/dist/jobs/_kie-upload.js +32 -0
- package/dist/jobs/_lease.js +163 -0
- package/dist/jobs/_persist-output.js +19 -0
- package/dist/jobs/_storage-retry.js +21 -0
- package/dist/jobs/_transcribe.js +245 -0
- package/dist/jobs/_translate-transcript.js +270 -0
- package/dist/jobs/analyze-alive-captions.js +10 -0
- package/dist/jobs/analyze-editing-moments.js +104 -0
- package/dist/jobs/analyze-jokes.js +143 -0
- package/dist/jobs/analyze-keyword-placement.js +155 -0
- package/dist/jobs/analyze-music.js +14 -0
- package/dist/jobs/analyze-video.js +282 -0
- package/dist/jobs/apply-cuts.js +83 -0
- package/dist/jobs/apply-sfx.js +107 -0
- package/dist/jobs/archive-clip.js +458 -0
- package/dist/jobs/assemble-goal.js +98 -0
- package/dist/jobs/audio-boost.js +71 -0
- package/dist/jobs/audio-mix.js +161 -0
- package/dist/jobs/backfill-asset-tags.js +144 -0
- package/dist/jobs/burn-watermark.js +316 -0
- package/dist/jobs/change-video-speed.js +176 -0
- package/dist/jobs/clean-video-audio.js +207 -0
- package/dist/jobs/cold-open.js +166 -0
- package/dist/jobs/combine-goal.js +108 -0
- package/dist/jobs/complete-provider-task.js +185 -0
- package/dist/jobs/copy-library-asset.js +33 -0
- package/dist/jobs/cost-estimate-assessment.js +113 -0
- package/dist/jobs/crop-image.js +47 -0
- package/dist/jobs/crop-video.js +71 -0
- package/dist/jobs/describe-image.js +181 -0
- package/dist/jobs/detect-scenes.js +33 -0
- package/dist/jobs/doctor-fix.js +100 -0
- package/dist/jobs/download-video.js +427 -0
- package/dist/jobs/enqueue-search-index.js +24 -0
- package/dist/jobs/even-split.js +122 -0
- package/dist/jobs/export-sequence.js +365 -0
- package/dist/jobs/extend-audio.js +229 -0
- package/dist/jobs/extract-audio.js +72 -0
- package/dist/jobs/extract-frame.js +58 -0
- package/dist/jobs/extract-notebook-audio.js +82 -0
- package/dist/jobs/extract-notebook-frame.js +93 -0
- package/dist/jobs/extract-video-clip.js +8 -0
- package/dist/jobs/extract-video-poster.js +55 -0
- package/dist/jobs/extract-waveform.js +25 -0
- package/dist/jobs/finish-goal.js +255 -0
- package/dist/jobs/forget-asset.js +11 -0
- package/dist/jobs/generate-acestep-cover.js +219 -0
- package/dist/jobs/generate-media.js +70 -0
- package/dist/jobs/image-subjects.js +1059 -0
- package/dist/jobs/index.js +18 -0
- package/dist/jobs/inject-first-frame.js +175 -0
- package/dist/jobs/isolate-vocals.js +69 -0
- package/dist/jobs/kie-paygate.js +177 -0
- package/dist/jobs/label-music-sections.js +5 -0
- package/dist/jobs/osg-placement.js +135 -0
- package/dist/jobs/pad-video.js +72 -0
- package/dist/jobs/plan-goal.js +97 -0
- package/dist/jobs/post-record.js +21 -0
- package/dist/jobs/post-to-x.js +21 -0
- package/dist/jobs/prepare-search-reference.js +190 -0
- package/dist/jobs/prepare-temporal-search.js +16 -0
- package/dist/jobs/process-audio.js +67 -0
- package/dist/jobs/process-image.js +58 -0
- package/dist/jobs/process-upload.js +166 -0
- package/dist/jobs/process-video.js +438 -0
- package/dist/jobs/register-all.js +161 -0
- package/dist/jobs/relayout-notebook.js +78 -0
- package/dist/jobs/remove-sound.js +299 -0
- package/dist/jobs/render-project.js +380 -0
- package/dist/jobs/render-qr-outro.js +138 -0
- package/dist/jobs/render-timeline.js +283 -0
- package/dist/jobs/repair-search-indexes.js +42 -0
- package/dist/jobs/replace-video-audio.js +64 -0
- package/dist/jobs/restore-archive-clip.js +53 -0
- package/dist/jobs/reverse-video.js +53 -0
- package/dist/jobs/rewrite-script.js +173 -0
- package/dist/jobs/run-notebook.js +3335 -0
- package/dist/jobs/semantic-index.js +797 -0
- package/dist/jobs/smooth-scene-concat.js +257 -0
- package/dist/jobs/splice-video.js +280 -0
- package/dist/jobs/split-video.js +127 -0
- package/dist/jobs/suggest-cuts.js +149 -0
- package/dist/jobs/suggest-sfx.js +135 -0
- package/dist/jobs/sync-video-to-audio.js +329 -0
- package/dist/jobs/tag-asset.js +266 -0
- package/dist/jobs/task-failure.js +16 -0
- package/dist/jobs/timewarp-video.js +152 -0
- package/dist/jobs/transcribe-video-audio.js +117 -0
- package/dist/jobs/transcribe.js +35 -0
- package/dist/jobs/translate-transcript.js +55 -0
- package/dist/jobs/trim-audio.js +133 -0
- package/dist/jobs/trim-video.js +291 -0
- package/dist/jobs/xai-paygate.js +26 -0
- package/dist/keyword-placement-cache.js +74 -0
- package/dist/lazy-render.js +142 -0
- package/dist/library-browse.js +78 -0
- package/dist/lightning-defaults.js +206 -0
- package/dist/load-env.js +12 -0
- package/dist/local-search-models.js +206 -0
- package/dist/local-upload.js +318 -0
- package/dist/logger.js +69 -0
- package/dist/media/adapters/alibaba.js +20 -0
- package/dist/media/adapters/elevenlabs.js +58 -0
- package/dist/media/adapters/fal.js +45 -0
- package/dist/media/adapters/index.js +26 -0
- package/dist/media/adapters/kie.js +120 -0
- package/dist/media/adapters/pending.js +21 -0
- package/dist/media/adapters/types.js +5 -0
- package/dist/media/adapters/xai.js +18 -0
- package/dist/media/catalog.js +236 -0
- package/dist/media/complete.js +191 -0
- package/dist/media/cost.js +45 -0
- package/dist/media/descriptor.js +157 -0
- package/dist/media/endpoints.js +11 -0
- package/dist/media/frame-options.js +18 -0
- package/dist/media/insert-video-timing.js +17 -0
- package/dist/media/json-schema.js +75 -0
- package/dist/media/legacy-task-types.js +129 -0
- package/dist/media/listing.js +24 -0
- package/dist/media/model-id.js +14 -0
- package/dist/media/preview.js +86 -0
- package/dist/media/prompt-dialects.js +65 -0
- package/dist/media/ref-fields.js +106 -0
- package/dist/media/refs.js +131 -0
- package/dist/media/request.js +188 -0
- package/dist/media/types.js +6 -0
- package/dist/media/wire.js +161 -0
- package/dist/media-library.js +535 -0
- package/dist/media-output-path.js +33 -0
- package/dist/multyculty-workflow.js +757 -0
- package/dist/notebook-action-catalog.js +608 -0
- package/dist/notebook-cell-outputs.js +726 -0
- package/dist/notebook-credit-retry.js +45 -0
- package/dist/notebook-fixtures.js +8 -0
- package/dist/notebook-generation-contract.js +77 -0
- package/dist/notebook-generation-controls.js +145 -0
- package/dist/notebook-goal-layout.js +40 -0
- package/dist/notebook-graph.js +37 -0
- package/dist/notebook-image-edit.js +84 -0
- package/dist/notebook-initial-generation-prompt.js +222 -0
- package/dist/notebook-live-analysis.js +941 -0
- package/dist/notebook-media-ops.js +579 -0
- package/dist/notebook-mutations.js +20 -0
- package/dist/notebook-placement.js +41 -0
- package/dist/notebook-provider-failure.js +87 -0
- package/dist/notebook-published-post.js +26 -0
- package/dist/notebook-reference-resolver.js +227 -0
- package/dist/notebook-relayout.js +270 -0
- package/dist/notebook-run-plan.js +519 -0
- package/dist/notebook-shots.js +162 -0
- package/dist/notebook-workflow.js +856 -0
- package/dist/one-password.js +200 -0
- package/dist/openai-intelligence.js +99 -0
- package/dist/original-media-file.js +12 -0
- package/dist/osg-assets.js +36 -0
- package/dist/osg-placement-cache.js +64 -0
- package/dist/osg-settings.js +27 -0
- package/dist/paid-tools.js +22 -0
- package/dist/platform-guidance.js +26 -0
- package/dist/posts/dropbox-auth.js +91 -0
- package/dist/posts/dropbox-upload.js +132 -0
- package/dist/posts/icloud-drive.js +134 -0
- package/dist/posts/icloud-status.js +64 -0
- package/dist/posts/post-contract.js +2 -0
- package/dist/posts/post-copy-format.js +43 -0
- package/dist/posts/post-copy.js +193 -0
- package/dist/posts/post-folder.js +18 -0
- package/dist/posts/post-metrics.js +119 -0
- package/dist/posts/post-store.js +340 -0
- package/dist/posts/x-post-target.js +43 -0
- package/dist/preview-payload.js +90 -0
- package/dist/prompt-surfaces.js +22 -0
- package/dist/provider-prompt-transform.js +77 -0
- package/dist/provider-status-link.js +21 -0
- package/dist/provider-task-record.js +78 -0
- package/dist/provider-task-status.js +47 -0
- package/dist/provider-usage.js +294 -0
- package/dist/queue.js +810 -0
- package/dist/reference-video-duration.js +17 -0
- package/dist/remotion/BlurFillVideo.js +51 -0
- package/dist/remotion/KeywordGraphicsOverlay.js +68 -0
- package/dist/remotion/OsgOverlay.js +30 -0
- package/dist/remotion/QrOutroComposition.js +47 -0
- package/dist/remotion/RenderComposition.js +11 -0
- package/dist/remotion/browser.js +59 -0
- package/dist/remotion/constants.js +35 -0
- package/dist/remotion/index.js +66 -0
- package/dist/remotion/keyword-graphics.js +151 -0
- package/dist/remotion/keyword-placement.js +182 -0
- package/dist/remotion/osg-spec.js +198 -0
- package/dist/remotion/public/qr-outro-impact.wav +0 -0
- package/dist/remotion/qr-outro-audio.js +1 -0
- package/dist/remotion/qr-outro-spec.js +211 -0
- package/dist/remotion/shared-bundle.js +37 -0
- package/dist/remotion/subtitles/AliveCaptions.js +83 -0
- package/dist/remotion/subtitles/Highlight.js +58 -0
- package/dist/remotion/subtitles/Impact.js +67 -0
- package/dist/remotion/subtitles/SubtitleOverlay.js +9 -0
- package/dist/remotion/subtitles/alive-layout.js +321 -0
- package/dist/remotion/subtitles/alive-types.js +17 -0
- package/dist/remotion/subtitles/chunker.js +68 -0
- package/dist/remotion/subtitles/segments.js +42 -0
- package/dist/remotion/subtitles/styles.js +4 -0
- package/dist/remotion/subtitles/types.js +1 -0
- package/dist/remotion/util.js +18 -0
- package/dist/remotion-cancellation.js +15 -0
- package/dist/rename-helpers.js +204 -0
- package/dist/rhythm/analyze.js +128 -0
- package/dist/rhythm/contract.js +14 -0
- package/dist/rhythm/draft-model.js +19 -0
- package/dist/rhythm/energy.js +133 -0
- package/dist/rhythm/fft.js +44 -0
- package/dist/rhythm/model.js +71 -0
- package/dist/rhythm/music-analysis.js +124 -0
- package/dist/rhythm/music-draft.js +261 -0
- package/dist/rhythm/onset.js +101 -0
- package/dist/rhythm/run-worker.js +51 -0
- package/dist/rhythm/score.js +47 -0
- package/dist/rhythm/section-labels.js +168 -0
- package/dist/rhythm/sequence-rhythm.js +67 -0
- package/dist/rhythm/synthesize.js +103 -0
- package/dist/rhythm/timing.js +98 -0
- package/dist/rhythm/track.js +320 -0
- package/dist/rhythm/visual-fit.js +4 -0
- package/dist/rhythm/worker.js +25 -0
- package/dist/scene-detect.js +200 -0
- package/dist/scene-transition.js +12 -0
- package/dist/scheduled-posts.js +200 -0
- package/dist/search-index-coverage.js +13 -0
- package/dist/search-index-media.js +71 -0
- package/dist/search-reference-store.js +60 -0
- package/dist/search-scoring.js +17 -0
- package/dist/serve.js +125 -0
- package/dist/server-lock.js +143 -0
- package/dist/server.js +42 -0
- package/dist/srt.js +16 -0
- package/dist/tag-backfill-state.js +121 -0
- package/dist/tag-settings.js +30 -0
- package/dist/tag-vocabulary.js +237 -0
- package/dist/test-queue-cleanup.js +1 -0
- package/dist/time-parse.js +50 -0
- package/dist/timeline-helpers.js +554 -0
- package/dist/timewarp.js +87 -0
- package/dist/tools/_image-inputs.js +9 -0
- package/dist/tools/_media-refs.js +84 -0
- package/dist/tools/_shared.js +59 -0
- package/dist/tools/_suggest-shared.js +279 -0
- package/dist/tools/add-timeline-audio.js +73 -0
- package/dist/tools/alive-captions.js +157 -0
- package/dist/tools/analyze-jokes.js +56 -0
- package/dist/tools/analyze-video.js +66 -0
- package/dist/tools/animate-keywords.js +217 -0
- package/dist/tools/api-status.js +42 -0
- package/dist/tools/application-commands.js +34 -0
- package/dist/tools/apply-cuts.js +112 -0
- package/dist/tools/apply-sfx.js +152 -0
- package/dist/tools/apply-watermark.js +670 -0
- package/dist/tools/archive-clip.js +74 -0
- package/dist/tools/asset-history.js +61 -0
- package/dist/tools/asset-manifest.js +24 -0
- package/dist/tools/asset-tags.js +151 -0
- package/dist/tools/assign-slot.js +97 -0
- package/dist/tools/audio-boost.js +107 -0
- package/dist/tools/audio-mix.js +121 -0
- package/dist/tools/book-usage.js +16 -0
- package/dist/tools/chain.js +136 -0
- package/dist/tools/change-video-speed.js +105 -0
- package/dist/tools/chat-history.js +5 -0
- package/dist/tools/chat.js +848 -0
- package/dist/tools/clean-video-audio.js +97 -0
- package/dist/tools/clear-failed-tasks.js +66 -0
- package/dist/tools/cold-open.js +114 -0
- package/dist/tools/command-plan.js +1680 -0
- package/dist/tools/commands.js +1666 -0
- package/dist/tools/concat-images.js +236 -0
- package/dist/tools/connect-service.js +55 -0
- package/dist/tools/copy-file-to-clipboard.js +58 -0
- package/dist/tools/create-character.js +114 -0
- package/dist/tools/create-qart.js +253 -0
- package/dist/tools/create-qr-outro.js +149 -0
- package/dist/tools/create-qrbtf.js +215 -0
- package/dist/tools/create-watermark.js +97 -0
- package/dist/tools/crop-asset.js +18 -0
- package/dist/tools/crop-image.js +120 -0
- package/dist/tools/crop-video.js +97 -0
- package/dist/tools/delete-asset.js +104 -0
- package/dist/tools/describe-image.js +69 -0
- package/dist/tools/detect-scenes.js +44 -0
- package/dist/tools/doctor.js +126 -0
- package/dist/tools/download-video.js +114 -0
- package/dist/tools/duplicate-asset.js +184 -0
- package/dist/tools/edit-timeline.js +59 -0
- package/dist/tools/editing-moments.js +105 -0
- package/dist/tools/estimate-cost.js +171 -0
- package/dist/tools/even-split.js +295 -0
- package/dist/tools/export-asset-file.js +22 -0
- package/dist/tools/export-asset.js +140 -0
- package/dist/tools/export-sequence.js +250 -0
- package/dist/tools/export-to-downloads.js +54 -0
- package/dist/tools/export-transcript-srt.js +79 -0
- package/dist/tools/extend-audio.js +100 -0
- package/dist/tools/extract-audio.js +174 -0
- package/dist/tools/extract-frame.js +108 -0
- package/dist/tools/extract-notebook-audio.js +173 -0
- package/dist/tools/extract-notebook-frame.js +160 -0
- package/dist/tools/extract-video-clip.js +101 -0
- package/dist/tools/generate-media.js +157 -0
- package/dist/tools/generation-payload.js +48 -0
- package/dist/tools/get-timeline.js +12 -0
- package/dist/tools/get-watermark-config.js +38 -0
- package/dist/tools/goal.js +644 -0
- package/dist/tools/image-subjects.js +711 -0
- package/dist/tools/index.js +323 -0
- package/dist/tools/inject-first-frame.js +154 -0
- package/dist/tools/invert-image.js +70 -0
- package/dist/tools/isolate-vocals.js +66 -0
- package/dist/tools/list-assets.js +98 -0
- package/dist/tools/list-cell-generations.js +225 -0
- package/dist/tools/list-elevenlabs-voices.js +163 -0
- package/dist/tools/log-wrapper.js +29 -0
- package/dist/tools/make-cover-song.js +95 -0
- package/dist/tools/mark-asset-seen.js +23 -0
- package/dist/tools/media-library.js +245 -0
- package/dist/tools/move-timeline-audio.js +35 -0
- package/dist/tools/music-analysis.js +60 -0
- package/dist/tools/music-draft.js +29 -0
- package/dist/tools/pad-video.js +87 -0
- package/dist/tools/post-record.js +83 -0
- package/dist/tools/post.js +168 -0
- package/dist/tools/prompt-history.js +164 -0
- package/dist/tools/provider-key-status.js +248 -0
- package/dist/tools/qrbtf-render.js +849 -0
- package/dist/tools/read-asset-metadata.js +26 -0
- package/dist/tools/read-file-at-commit.js +29 -0
- package/dist/tools/read-transcript.js +41 -0
- package/dist/tools/reconcile-project-work.js +1452 -0
- package/dist/tools/recover-asset.js +28 -0
- package/dist/tools/remove-sound.js +72 -0
- package/dist/tools/remove-timeline-audio.js +34 -0
- package/dist/tools/remove-timeline-slot.js +40 -0
- package/dist/tools/rename-all.js +434 -0
- package/dist/tools/rename-asset.js +34 -0
- package/dist/tools/render-settings.js +47 -0
- package/dist/tools/render-timeline.js +120 -0
- package/dist/tools/render.js +384 -0
- package/dist/tools/reorder-character-images.js +45 -0
- package/dist/tools/replace-video-audio.js +133 -0
- package/dist/tools/restore-archive-clips.js +209 -0
- package/dist/tools/restore-asset-version.js +37 -0
- package/dist/tools/retry-generation.js +54 -0
- package/dist/tools/reverse-video.js +74 -0
- package/dist/tools/rewrite-prompt.js +388 -0
- package/dist/tools/rewrite-script.js +67 -0
- package/dist/tools/schema-from-apicity.js +66 -0
- package/dist/tools/search-archive.js +150 -0
- package/dist/tools/semantic-search.js +1214 -0
- package/dist/tools/sequence-rhythm.js +43 -0
- package/dist/tools/set-character-metadata.js +84 -0
- package/dist/tools/set-env-secret.js +40 -0
- package/dist/tools/set-osg.js +143 -0
- package/dist/tools/show-folder.js +37 -0
- package/dist/tools/smooth-scene-concat.js +122 -0
- package/dist/tools/splice-video.js +149 -0
- package/dist/tools/split-video.js +202 -0
- package/dist/tools/suggest-cuts.js +45 -0
- package/dist/tools/suggest-sfx.js +43 -0
- package/dist/tools/sync-video-to-audio.js +115 -0
- package/dist/tools/tag-backfill.js +177 -0
- package/dist/tools/timewarp-video.js +85 -0
- package/dist/tools/tool-def.js +1 -0
- package/dist/tools/tool-json-schema.js +116 -0
- package/dist/tools/transcribe.js +90 -0
- package/dist/tools/translate-subtitles.js +59 -0
- package/dist/tools/trim-audio.js +139 -0
- package/dist/tools/trim-video.js +168 -0
- package/dist/tools/update-script.js +25 -0
- package/dist/tools/update-timeline-audio.js +40 -0
- package/dist/tools/upload-file.js +41 -0
- package/dist/tools/verify-x-mentions.js +141 -0
- package/dist/tools/video-editing-command-contract.js +291 -0
- package/dist/tools/video-source-reference.js +30 -0
- package/dist/tools/video-time-input.js +18 -0
- package/dist/tools/videobook-domain.js +2021 -0
- package/dist/tools/wait-for-asset-ready.js +143 -0
- package/dist/tools/workspace-actions.js +54 -0
- package/dist/transcript-projection.js +202 -0
- package/dist/transcript-srt.js +33 -0
- package/dist/upload.js +76 -0
- package/dist/usage-providers.js +25 -0
- package/dist/video-analysis-projection.js +46 -0
- package/dist/video-poster.js +33 -0
- package/dist/video-proxy.js +99 -0
- package/dist/vision-regions.js +111 -0
- package/dist/voice-actions.js +90 -0
- package/dist/voice-audio.js +55 -0
- package/dist/voice-config.js +122 -0
- package/dist/voice-continuation.js +54 -0
- package/dist/voice-contract.js +1 -0
- package/dist/voice-http.js +145 -0
- package/dist/voice-reply-progress.js +55 -0
- package/dist/voice-session.js +992 -0
- package/dist/watermark-badge.js +259 -0
- package/dist/watermark-config.js +111 -0
- package/dist/watermark-handle-position.js +55 -0
- package/dist/watermark-platform.js +15 -0
- package/dist/watermark-timing.js +16 -0
- package/dist/workspace-action-catalog.js +315 -0
- package/dist/workspace-action-contract.js +21 -0
- package/dist/workspace-action-groups.js +98 -0
- package/dist/workspace-action-surfaces.js +7 -0
- package/dist/x/account.js +88 -0
- package/dist/x/authenticated-handle.js +30 -0
- package/dist/x/oauth-flow.js +191 -0
- package/dist/x/post-contract.js +1 -0
- package/dist/x/post-metadata.js +44 -0
- package/dist/x/post-to-x.js +456 -0
- package/dist/x/published-post-cell.js +90 -0
- package/dist/x/x-tokens.js +69 -0
- package/dist/ytdlp-binary.js +65 -0
- package/dist/zod-slots.js +334 -0
- package/package.json +84 -4
- package/ui/assets/alive-captions-BWb5nLKj.css +1 -0
- package/ui/assets/alive-captions-DPEr1nKM.js +1 -0
- package/ui/assets/confirm-bodies-D8ouHXKy.js +1 -0
- package/ui/assets/confirm-bodies-DrMhaXC6.css +1 -0
- package/ui/assets/fullscreen-B7nw4YbE.js +1 -0
- package/ui/assets/goal-ui-DK-xTCK0.js +2 -0
- package/ui/assets/goal-ui-DXdUM2KF.css +1 -0
- package/ui/assets/index-DzPAnCUj.css +1 -0
- package/ui/assets/index-kS1905s5.js +37 -0
- package/ui/assets/inspector-MlzQbTVo.js +1 -0
- package/ui/assets/jobs-ui-CoinOWx0.js +1 -0
- package/ui/assets/music-video-Cbe80kmQ.js +1 -0
- package/ui/assets/music-video-DPaiKKEq.css +1 -0
- package/ui/assets/ui-Oirr9VOf.css +1 -0
- package/ui/assets/ui-xo_Y1yQg.js +2 -0
- package/ui/assets/vendor-D2rfiZ5j.css +1 -0
- package/ui/assets/vendor-DfTCWgKm.js +19 -0
- package/ui/assets/vendor-flow-BXbeWxW7.js +7 -0
- package/ui/assets/vendor-flow-BnuhLJ6X.css +1 -0
- package/ui/assets/vendor-icons-CGqhCIXV.js +1 -0
- package/ui/assets/vendor-mcp-BTlEJcHV.js +67 -0
- package/ui/assets/vendor-react-Dj7D1fD4.js +9 -0
- package/ui/assets/vendor-remotion-CH1HXrlc.js +95 -0
- package/ui/assets/videobook-Qlp9HQFu.js +20 -0
- package/ui/favicon.svg +8 -0
- package/ui/index.html +126 -0
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { RHYTHM_ANALYZER_ID, RHYTHM_ANALYZER_VERSION, RHYTHM_LATENCY_SAMPLES, RHYTHM_SCHEMA_VERSION, } from "./contract.js";
|
|
2
|
+
import { energyCurve } from "./energy.js";
|
|
3
|
+
import { beatPositions } from "./timing.js";
|
|
4
|
+
const MIN_BPM = 55;
|
|
5
|
+
const MAX_BPM = 200;
|
|
6
|
+
const TIGHTNESS = 80;
|
|
7
|
+
const LOW_CONFIDENCE = 0.42;
|
|
8
|
+
export function trackBeats(envelope) {
|
|
9
|
+
const hopSeconds = envelope.hopSamples / envelope.sampleRate;
|
|
10
|
+
const latencySeconds = RHYTHM_LATENCY_SAMPLES / envelope.sampleRate;
|
|
11
|
+
const onsets = pickOnsets(envelope.strength, hopSeconds);
|
|
12
|
+
const tempo = estimateTempo(envelope.strength, hopSeconds, onsets);
|
|
13
|
+
const tracked = tempo.periodHops === null
|
|
14
|
+
? []
|
|
15
|
+
: beatTrack(envelope.strength, tempo.periodHops);
|
|
16
|
+
const beatTimes = tracked
|
|
17
|
+
.map((frame) => frame * hopSeconds)
|
|
18
|
+
.filter((time) => time >= 0);
|
|
19
|
+
const confidences = beatConfidences(envelope.strength, tracked);
|
|
20
|
+
const global = globalConfidence(confidences, envelope.strength, tracked);
|
|
21
|
+
const weakBeats = global < LOW_CONFIDENCE;
|
|
22
|
+
const noBeats = tempo.bpm === null || global <= 0 || beatTimes.length < (weakBeats ? 8 : 2);
|
|
23
|
+
const lowConfidence = noBeats || weakBeats || beatTimes.length < 8 || tempo.ambiguity !== "stable";
|
|
24
|
+
const beats = noBeats
|
|
25
|
+
? []
|
|
26
|
+
: beatTimes.map((timeSeconds, index) => ({
|
|
27
|
+
timeSeconds,
|
|
28
|
+
confidence: confidences[index] ?? null,
|
|
29
|
+
}));
|
|
30
|
+
const positions = beatPositions(beats, tempo.bpm);
|
|
31
|
+
const detectedMeter = noBeats || weakBeats ? null : detectMeter(envelope.strength, tracked, positions);
|
|
32
|
+
const assignedMeter = detectedMeter ?? (noBeats ? null : { beatsPerBar: 4, confidence: 0, phase: 0 });
|
|
33
|
+
const meter = assignedMeter
|
|
34
|
+
? { beatsPerBar: assignedMeter.beatsPerBar, confidence: assignedMeter.confidence }
|
|
35
|
+
: null;
|
|
36
|
+
const bars = assignedMeter ? barStarts(beats, positions, assignedMeter) : null;
|
|
37
|
+
return {
|
|
38
|
+
schemaVersion: RHYTHM_SCHEMA_VERSION,
|
|
39
|
+
analyzerId: RHYTHM_ANALYZER_ID,
|
|
40
|
+
analyzerVersion: RHYTHM_ANALYZER_VERSION,
|
|
41
|
+
sampleRate: envelope.sampleRate,
|
|
42
|
+
hopSamples: envelope.hopSamples,
|
|
43
|
+
windowSamples: envelope.windowSamples,
|
|
44
|
+
latencySeconds,
|
|
45
|
+
beats,
|
|
46
|
+
onsets,
|
|
47
|
+
bpm: noBeats ? null : tempo.bpm,
|
|
48
|
+
bpmConfidence: tempo.confidence,
|
|
49
|
+
tempoAmbiguity: tempo.ambiguity,
|
|
50
|
+
meter: noBeats ? null : meter,
|
|
51
|
+
bars: noBeats ? null : bars,
|
|
52
|
+
barSource: noBeats ? null : detectedMeter ? "detected" : "estimated",
|
|
53
|
+
noBeats,
|
|
54
|
+
lowConfidence,
|
|
55
|
+
energy: energyCurve(envelope),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function pickOnsets(strength, hopSeconds) {
|
|
59
|
+
if (strength.length < 3)
|
|
60
|
+
return [];
|
|
61
|
+
let mean = 0;
|
|
62
|
+
for (const value of strength)
|
|
63
|
+
mean += value;
|
|
64
|
+
mean /= strength.length;
|
|
65
|
+
let variance = 0;
|
|
66
|
+
for (const value of strength)
|
|
67
|
+
variance += (value - mean) ** 2;
|
|
68
|
+
const deviation = Math.sqrt(variance / strength.length);
|
|
69
|
+
const threshold = Math.max(0.08, mean + deviation * 0.4);
|
|
70
|
+
const minGap = Math.max(1, Math.round(0.05 / hopSeconds));
|
|
71
|
+
const onsets = [];
|
|
72
|
+
let last = -minGap;
|
|
73
|
+
for (let index = 1; index < strength.length - 1; index += 1) {
|
|
74
|
+
const value = strength[index];
|
|
75
|
+
if (value < threshold)
|
|
76
|
+
continue;
|
|
77
|
+
if (value < strength[index - 1] || value < strength[index + 1])
|
|
78
|
+
continue;
|
|
79
|
+
if (index - last < minGap) {
|
|
80
|
+
const previous = onsets[onsets.length - 1];
|
|
81
|
+
if (previous && value > previous.strength) {
|
|
82
|
+
onsets[onsets.length - 1] = { timeSeconds: index * hopSeconds, strength: value };
|
|
83
|
+
last = index;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
onsets.push({ timeSeconds: index * hopSeconds, strength: value });
|
|
88
|
+
last = index;
|
|
89
|
+
}
|
|
90
|
+
return onsets;
|
|
91
|
+
}
|
|
92
|
+
function estimateTempo(strength, hopSeconds, onsets) {
|
|
93
|
+
const minLag = Math.max(2, Math.round(60 / MAX_BPM / hopSeconds));
|
|
94
|
+
const maxLag = Math.min(strength.length - 2, Math.round(60 / MIN_BPM / hopSeconds));
|
|
95
|
+
if (maxLag <= minLag + 1 || onsets.length < 2) {
|
|
96
|
+
return { bpm: null, confidence: null, ambiguity: null, periodHops: null };
|
|
97
|
+
}
|
|
98
|
+
const correlation = autocorrelate(strength, minLag, maxLag);
|
|
99
|
+
let bestLag = minLag;
|
|
100
|
+
let best = -Infinity;
|
|
101
|
+
for (let lag = minLag; lag <= maxLag; lag += 1) {
|
|
102
|
+
const value = correlation[lag - minLag];
|
|
103
|
+
if (value > best) {
|
|
104
|
+
best = value;
|
|
105
|
+
bestLag = lag;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const onsetPeriod = onsetInterval(onsets, hopSeconds, minLag, maxLag);
|
|
109
|
+
const resolved = resolveOctave(bestLag, correlation, minLag, onsetPeriod);
|
|
110
|
+
const bpm = 60 / (resolved.lag * hopSeconds);
|
|
111
|
+
const mean = correlation.reduce((sum, value) => sum + value, 0) / correlation.length;
|
|
112
|
+
const peakiness = best <= 0 ? 0 : (best - mean) / best;
|
|
113
|
+
const confidence = best <= 0 ? 0 : Math.max(0, Math.min(1, peakiness));
|
|
114
|
+
if (peakiness < 0.55) {
|
|
115
|
+
return { bpm: null, confidence, ambiguity: null, periodHops: null };
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
bpm,
|
|
119
|
+
confidence,
|
|
120
|
+
ambiguity: resolved.ambiguity,
|
|
121
|
+
periodHops: resolved.lag,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
function autocorrelate(strength, minLag, maxLag) {
|
|
125
|
+
const values = new Float64Array(maxLag - minLag + 1);
|
|
126
|
+
let energy = 0;
|
|
127
|
+
for (const value of strength)
|
|
128
|
+
energy += value * value;
|
|
129
|
+
const norm = Math.sqrt(energy) || 1;
|
|
130
|
+
for (let lag = minLag; lag <= maxLag; lag += 1) {
|
|
131
|
+
let sum = 0;
|
|
132
|
+
for (let index = 0; index + lag < strength.length; index += 1) {
|
|
133
|
+
sum += strength[index] * strength[index + lag];
|
|
134
|
+
}
|
|
135
|
+
values[lag - minLag] = sum / norm;
|
|
136
|
+
}
|
|
137
|
+
return values;
|
|
138
|
+
}
|
|
139
|
+
function onsetInterval(onsets, hopSeconds, minLag, maxLag) {
|
|
140
|
+
const gaps = [];
|
|
141
|
+
for (let index = 1; index < onsets.length; index += 1) {
|
|
142
|
+
const hops = Math.round((onsets[index].timeSeconds - onsets[index - 1].timeSeconds) / hopSeconds);
|
|
143
|
+
if (hops >= minLag && hops <= maxLag)
|
|
144
|
+
gaps.push(hops);
|
|
145
|
+
}
|
|
146
|
+
if (gaps.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
gaps.sort((left, right) => left - right);
|
|
149
|
+
return gaps[Math.floor(gaps.length / 2)];
|
|
150
|
+
}
|
|
151
|
+
function resolveOctave(lag, correlation, minLag, onsetPeriod) {
|
|
152
|
+
const candidates = [lag, Math.round(lag / 2), lag * 2].filter((value) => {
|
|
153
|
+
const index = value - minLag;
|
|
154
|
+
return value >= minLag && index >= 0 && index < correlation.length;
|
|
155
|
+
});
|
|
156
|
+
const unique = [...new Set(candidates)];
|
|
157
|
+
let chosen = lag;
|
|
158
|
+
let bestScore = -Infinity;
|
|
159
|
+
for (const candidate of unique) {
|
|
160
|
+
const auto = correlation[candidate - minLag] ?? 0;
|
|
161
|
+
const onsetScore = onsetPeriod === null
|
|
162
|
+
? 0
|
|
163
|
+
: 1 - Math.min(1, Math.abs(candidate - onsetPeriod) / onsetPeriod);
|
|
164
|
+
const score = auto + onsetScore * Math.max(auto, 1);
|
|
165
|
+
if (score > bestScore) {
|
|
166
|
+
bestScore = score;
|
|
167
|
+
chosen = candidate;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
let ambiguity = "stable";
|
|
171
|
+
const half = correlation[Math.round(chosen / 2) - minLag];
|
|
172
|
+
const double = correlation[chosen * 2 - minLag];
|
|
173
|
+
const self = correlation[chosen - minLag] ?? 0;
|
|
174
|
+
if (half !== undefined && self > 0 && half > self * 0.92)
|
|
175
|
+
ambiguity = "half";
|
|
176
|
+
else if (double !== undefined && self > 0 && double > self * 0.92)
|
|
177
|
+
ambiguity = "double";
|
|
178
|
+
if (chosen !== lag && chosen < lag)
|
|
179
|
+
ambiguity = "double";
|
|
180
|
+
if (chosen !== lag && chosen > lag)
|
|
181
|
+
ambiguity = "half";
|
|
182
|
+
return { lag: chosen, ambiguity };
|
|
183
|
+
}
|
|
184
|
+
function beatTrack(strength, period) {
|
|
185
|
+
const count = strength.length;
|
|
186
|
+
const score = new Float64Array(count);
|
|
187
|
+
const back = new Int32Array(count);
|
|
188
|
+
back.fill(-1);
|
|
189
|
+
const minLag = Math.max(1, Math.round(period * 0.55));
|
|
190
|
+
const maxLag = Math.max(minLag + 1, Math.round(period * 1.7));
|
|
191
|
+
for (let index = 0; index < count; index += 1) {
|
|
192
|
+
let best = strength[index];
|
|
193
|
+
let link = -1;
|
|
194
|
+
const start = Math.max(0, index - maxLag);
|
|
195
|
+
const end = index - minLag;
|
|
196
|
+
for (let previous = start; previous <= end; previous += 1) {
|
|
197
|
+
const lag = index - previous;
|
|
198
|
+
const penalty = TIGHTNESS * (Math.log(lag / period) ** 2);
|
|
199
|
+
const candidate = score[previous] + strength[index] - penalty;
|
|
200
|
+
if (candidate > best) {
|
|
201
|
+
best = candidate;
|
|
202
|
+
link = previous;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
score[index] = link < 0 ? strength[index] : best;
|
|
206
|
+
back[index] = link;
|
|
207
|
+
}
|
|
208
|
+
let cursor = -1;
|
|
209
|
+
let bestScore = -Infinity;
|
|
210
|
+
const tail = Math.max(0, count - Math.round(period * 2));
|
|
211
|
+
for (let index = tail; index < count; index += 1) {
|
|
212
|
+
if (score[index] > bestScore) {
|
|
213
|
+
bestScore = score[index];
|
|
214
|
+
cursor = index;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const frames = [];
|
|
218
|
+
const guard = new Set();
|
|
219
|
+
while (cursor >= 0 && !guard.has(cursor)) {
|
|
220
|
+
guard.add(cursor);
|
|
221
|
+
if (strength[cursor] > 0.02)
|
|
222
|
+
frames.push(cursor);
|
|
223
|
+
cursor = back[cursor];
|
|
224
|
+
}
|
|
225
|
+
frames.reverse();
|
|
226
|
+
return thinBeats(frames, period);
|
|
227
|
+
}
|
|
228
|
+
function thinBeats(frames, period) {
|
|
229
|
+
const minimum = Math.max(1, Math.round(period * 0.55));
|
|
230
|
+
const kept = [];
|
|
231
|
+
for (const frame of frames) {
|
|
232
|
+
const previous = kept[kept.length - 1];
|
|
233
|
+
if (previous === undefined || frame - previous >= minimum)
|
|
234
|
+
kept.push(frame);
|
|
235
|
+
else
|
|
236
|
+
kept[kept.length - 1] = frame;
|
|
237
|
+
}
|
|
238
|
+
return kept;
|
|
239
|
+
}
|
|
240
|
+
function beatConfidences(strength, frames) {
|
|
241
|
+
return frames.map((frame) => {
|
|
242
|
+
const value = strength[frame] ?? 0;
|
|
243
|
+
return value > 0 ? Math.max(0, Math.min(1, value)) : null;
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
function globalConfidence(confidences, strength, frames) {
|
|
247
|
+
if (frames.length < 2)
|
|
248
|
+
return 0;
|
|
249
|
+
let sum = 0;
|
|
250
|
+
let known = 0;
|
|
251
|
+
for (const value of confidences) {
|
|
252
|
+
if (value === null)
|
|
253
|
+
continue;
|
|
254
|
+
sum += value;
|
|
255
|
+
known += 1;
|
|
256
|
+
}
|
|
257
|
+
if (known === 0)
|
|
258
|
+
return 0;
|
|
259
|
+
const mean = sum / known;
|
|
260
|
+
let noise = 0;
|
|
261
|
+
let samples = 0;
|
|
262
|
+
for (let index = 0; index < strength.length; index += 8) {
|
|
263
|
+
noise += strength[index];
|
|
264
|
+
samples += 1;
|
|
265
|
+
}
|
|
266
|
+
const floor = samples ? noise / samples : 0;
|
|
267
|
+
return Math.max(0, Math.min(1, mean - floor));
|
|
268
|
+
}
|
|
269
|
+
function detectMeter(strength, frames, positions) {
|
|
270
|
+
if (frames.length < 8)
|
|
271
|
+
return null;
|
|
272
|
+
let best = null;
|
|
273
|
+
for (const beatsPerBar of [3, 4, 5, 6]) {
|
|
274
|
+
if (frames.length < beatsPerBar * 4)
|
|
275
|
+
continue;
|
|
276
|
+
let bestPhase = 0;
|
|
277
|
+
let bestMean = -Infinity;
|
|
278
|
+
let other = 0;
|
|
279
|
+
for (let phase = 0; phase < beatsPerBar; phase += 1) {
|
|
280
|
+
let sum = 0;
|
|
281
|
+
let count = 0;
|
|
282
|
+
for (let index = 0; index < frames.length; index += 1) {
|
|
283
|
+
if (positions[index] % beatsPerBar !== phase)
|
|
284
|
+
continue;
|
|
285
|
+
sum += strength[frames[index]] ?? 0;
|
|
286
|
+
count += 1;
|
|
287
|
+
}
|
|
288
|
+
const mean = count ? sum / count : 0;
|
|
289
|
+
if (mean > bestMean) {
|
|
290
|
+
other = bestMean;
|
|
291
|
+
bestMean = mean;
|
|
292
|
+
bestPhase = phase;
|
|
293
|
+
}
|
|
294
|
+
else if (mean > other) {
|
|
295
|
+
other = mean;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
const confidence = bestMean <= 0 ? 0 : (bestMean - Math.max(0, other)) / bestMean;
|
|
299
|
+
if (confidence < 0.18)
|
|
300
|
+
continue;
|
|
301
|
+
if (!best || confidence > best.confidence) {
|
|
302
|
+
best = { beatsPerBar, confidence, phase: bestPhase };
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
if (!best || best.confidence < 0.4)
|
|
306
|
+
return null;
|
|
307
|
+
return best;
|
|
308
|
+
}
|
|
309
|
+
function barStarts(beats, positions, meter) {
|
|
310
|
+
const bars = [];
|
|
311
|
+
for (let index = 0; index < beats.length; index += 1) {
|
|
312
|
+
if (positions[index] % meter.beatsPerBar !== meter.phase)
|
|
313
|
+
continue;
|
|
314
|
+
const beat = beats[index];
|
|
315
|
+
if (!beat)
|
|
316
|
+
continue;
|
|
317
|
+
bars.push({ timeSeconds: beat.timeSeconds, confidence: meter.confidence });
|
|
318
|
+
}
|
|
319
|
+
return bars.length ? bars : null;
|
|
320
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { analyzeAudioFile } from "./analyze.js";
|
|
2
|
+
const controller = new AbortController();
|
|
3
|
+
process.once("SIGTERM", () => controller.abort());
|
|
4
|
+
process.once("disconnect", () => controller.abort());
|
|
5
|
+
async function send(message) {
|
|
6
|
+
if (!process.connected || !process.send)
|
|
7
|
+
return;
|
|
8
|
+
await new Promise((resolve, reject) => {
|
|
9
|
+
process.send(message, (error) => error ? reject(error) : resolve());
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
process.once("message", async (message) => {
|
|
13
|
+
try {
|
|
14
|
+
const analysis = await analyzeAudioFile(message.ffmpegPath, message.inputPath, controller.signal, message.streamIndex);
|
|
15
|
+
await send({ analysis });
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
if (process.connected)
|
|
19
|
+
await send({ error: error instanceof Error ? error.message : String(error) });
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
if (process.connected)
|
|
23
|
+
process.disconnect?.();
|
|
24
|
+
}
|
|
25
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { FFMPEG } from "./ffmpeg-binary.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
// Camera-cut threshold on scdet's absolute mean frame diff. ffmpeg's default
|
|
6
|
+
// scdet threshold is 10; we match that for the "shot" classification. Anything
|
|
7
|
+
// below this within a single shot is candidate territory for "settle".
|
|
8
|
+
const SHOT_SCORE_THRESHOLD = 10;
|
|
9
|
+
// scdet runs at this temporal resolution. 4 fps gives 0.25s candidate
|
|
10
|
+
// granularity — enough to land action endings cleanly while keeping the
|
|
11
|
+
// detection pass fast on long clips.
|
|
12
|
+
const SCDET_FPS = 4;
|
|
13
|
+
// Settle: within a single shot, look for the falling edge of motion. The
|
|
14
|
+
// frame must drop below the segment's 25th percentile after sustained motion
|
|
15
|
+
// above the 75th percentile, AND stay below p25 for at least this many
|
|
16
|
+
// seconds. Tuned so a short pause inside an action doesn't produce a false
|
|
17
|
+
// settle, but a real "the man finished standing up" landing does.
|
|
18
|
+
const SETTLE_HOLD_S = 0.5;
|
|
19
|
+
// Parse scdet's stderr stream. ffmpeg 6's scdet logs one line per processed
|
|
20
|
+
// frame in the form:
|
|
21
|
+
// [scdet @ 0x...] lavfi.scd.score: 4.798, lavfi.scd.time: 0.25
|
|
22
|
+
// Score is the absolute mean frame difference (Y-plane), time is seconds.
|
|
23
|
+
// The very first line always has score=0 (no previous frame) — we keep it
|
|
24
|
+
// in the timeline; deriveCandidates ignores it via the threshold.
|
|
25
|
+
function parseScdetOutput(stderr) {
|
|
26
|
+
const re = /lavfi\.scd\.score:\s*([0-9.]+),\s*lavfi\.scd\.time:\s*([0-9.]+)/g;
|
|
27
|
+
const out = [];
|
|
28
|
+
let m;
|
|
29
|
+
while ((m = re.exec(stderr)) !== null) {
|
|
30
|
+
const score = parseFloat(m[1]);
|
|
31
|
+
const t = parseFloat(m[2]);
|
|
32
|
+
if (Number.isFinite(score) && Number.isFinite(t))
|
|
33
|
+
out.push({ t, score });
|
|
34
|
+
}
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
export function deriveCameraCandidates(samples, scoreFloor = 6, clusterSeconds = 0.5) {
|
|
38
|
+
const peaks = samples.filter((sample, index) => {
|
|
39
|
+
if (sample.score < scoreFloor)
|
|
40
|
+
return false;
|
|
41
|
+
const previous = samples[index - 1]?.score ?? Number.NEGATIVE_INFINITY;
|
|
42
|
+
const next = samples[index + 1]?.score ?? Number.NEGATIVE_INFINITY;
|
|
43
|
+
return sample.score >= previous && sample.score >= next;
|
|
44
|
+
});
|
|
45
|
+
const clustered = [];
|
|
46
|
+
for (const peak of peaks) {
|
|
47
|
+
const previous = clustered.at(-1);
|
|
48
|
+
if (!previous || peak.t - previous.t > clusterSeconds) {
|
|
49
|
+
clustered.push({ ...peak });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (peak.score > previous.score) {
|
|
53
|
+
clustered[clustered.length - 1] = { ...peak };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return clustered;
|
|
57
|
+
}
|
|
58
|
+
// Two-tier candidate derivation from a per-frame scdet timeline:
|
|
59
|
+
//
|
|
60
|
+
// 1. shot — any frame whose scdet score is ≥ SHOT_SCORE_THRESHOLD. These
|
|
61
|
+
// are camera/cut changes. ffmpeg's default scdet threshold is the
|
|
62
|
+
// same value; matching it keeps behavior aligned with the upstream
|
|
63
|
+
// definition of "scene change".
|
|
64
|
+
//
|
|
65
|
+
// 2. settle — within each run of consecutive non-shot frames (a single shot),
|
|
66
|
+
// find a falling edge: a frame where the score drops below the
|
|
67
|
+
// segment's 25th percentile after sustained motion above the 75th
|
|
68
|
+
// percentile, and stays below p25 for ≥ SETTLE_HOLD_S. The first
|
|
69
|
+
// frame of the sustained-low region is the action's natural end.
|
|
70
|
+
//
|
|
71
|
+
// Settle scoring is (above-mean − below-mean), so deeper drops rank higher.
|
|
72
|
+
// Empty / quiet shots produce no settle candidates.
|
|
73
|
+
function deriveCandidates(samples) {
|
|
74
|
+
if (samples.length === 0)
|
|
75
|
+
return [];
|
|
76
|
+
const out = [];
|
|
77
|
+
const shotIdx = [];
|
|
78
|
+
for (let i = 0; i < samples.length; i++) {
|
|
79
|
+
if (samples[i].score >= SHOT_SCORE_THRESHOLD) {
|
|
80
|
+
shotIdx.push(i);
|
|
81
|
+
out.push({ t: samples[i].t, score: samples[i].score, kind: "shot" });
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
const segments = [];
|
|
85
|
+
let segStart = 0;
|
|
86
|
+
for (const i of shotIdx) {
|
|
87
|
+
if (i > segStart)
|
|
88
|
+
segments.push([segStart, i]);
|
|
89
|
+
segStart = i + 1;
|
|
90
|
+
}
|
|
91
|
+
if (segStart < samples.length)
|
|
92
|
+
segments.push([segStart, samples.length]);
|
|
93
|
+
for (const [lo, hi] of segments) {
|
|
94
|
+
const seg = samples.slice(lo, hi);
|
|
95
|
+
if (seg.length < 3)
|
|
96
|
+
continue;
|
|
97
|
+
const sorted = [...seg.map((s) => s.score)].sort((a, b) => a - b);
|
|
98
|
+
const p25 = sorted[Math.floor(sorted.length * 0.25)];
|
|
99
|
+
const p75 = sorted[Math.floor(sorted.length * 0.75)];
|
|
100
|
+
if (p75 <= p25)
|
|
101
|
+
continue;
|
|
102
|
+
let aboveSum = 0;
|
|
103
|
+
let aboveCount = 0;
|
|
104
|
+
for (let j = 0; j < seg.length; j++) {
|
|
105
|
+
if (seg[j].score >= p75) {
|
|
106
|
+
aboveSum += seg[j].score;
|
|
107
|
+
aboveCount++;
|
|
108
|
+
}
|
|
109
|
+
if (aboveCount > 0 && seg[j].score <= p25) {
|
|
110
|
+
const start = seg[j].t;
|
|
111
|
+
let k = j;
|
|
112
|
+
while (k < seg.length && seg[k].score <= p25 && seg[k].t - start < SETTLE_HOLD_S)
|
|
113
|
+
k++;
|
|
114
|
+
if (k < seg.length && seg[k].t - start >= SETTLE_HOLD_S) {
|
|
115
|
+
let belowSum = 0;
|
|
116
|
+
let belowCount = 0;
|
|
117
|
+
for (let m = j; m < k; m++) {
|
|
118
|
+
belowSum += seg[m].score;
|
|
119
|
+
belowCount++;
|
|
120
|
+
}
|
|
121
|
+
const aboveMean = aboveSum / aboveCount;
|
|
122
|
+
const belowMean = belowCount > 0 ? belowSum / belowCount : 0;
|
|
123
|
+
out.push({ t: seg[j].t, score: Math.max(0, aboveMean - belowMean), kind: "settle" });
|
|
124
|
+
j = k;
|
|
125
|
+
aboveSum = 0;
|
|
126
|
+
aboveCount = 0;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
out.sort((a, b) => a.t - b.t);
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
// Run a single ffmpeg scdet pass over the input video at downscaled
|
|
135
|
+
// 320×180 / SCDET_FPS, parse the per-frame score timeline from stderr, and
|
|
136
|
+
// return shot + settle candidates. ffmpeg writes scdet's metadata to stderr
|
|
137
|
+
// (not stdout), so we capture stderr from the invocation. A non-zero exit is
|
|
138
|
+
// expected for `-f null -` runs only when ffmpeg actually fails — successful
|
|
139
|
+
// runs return cleanly with the metadata in stderr.
|
|
140
|
+
export async function detectSceneSamples(videoPath, options = {}) {
|
|
141
|
+
let stderr = "";
|
|
142
|
+
const startSeconds = Math.max(0, options.startSeconds ?? 0);
|
|
143
|
+
const args = [
|
|
144
|
+
"-hide_banner",
|
|
145
|
+
"-nostats",
|
|
146
|
+
"-loglevel", "info",
|
|
147
|
+
];
|
|
148
|
+
if (startSeconds > 0)
|
|
149
|
+
args.push("-ss", String(startSeconds));
|
|
150
|
+
args.push("-i", videoPath);
|
|
151
|
+
if (typeof options.durationSeconds === "number"
|
|
152
|
+
&& options.durationSeconds > 0) {
|
|
153
|
+
args.push("-t", String(options.durationSeconds));
|
|
154
|
+
}
|
|
155
|
+
const filters = ["scale=320:180"];
|
|
156
|
+
if (typeof options.fps === "number" && options.fps > 0) {
|
|
157
|
+
filters.push(`fps=${options.fps}`);
|
|
158
|
+
}
|
|
159
|
+
filters.push("scdet=threshold=0");
|
|
160
|
+
args.push("-vf", filters.join(","), "-an", "-f", "null", "-");
|
|
161
|
+
try {
|
|
162
|
+
const result = await execFileAsync(FFMPEG, args, {
|
|
163
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
164
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
165
|
+
});
|
|
166
|
+
stderr = result.stderr;
|
|
167
|
+
}
|
|
168
|
+
catch (error) {
|
|
169
|
+
if (options.signal?.aborted) {
|
|
170
|
+
throw options.signal.reason instanceof Error
|
|
171
|
+
? options.signal.reason
|
|
172
|
+
: new Error("Scene detection was cancelled");
|
|
173
|
+
}
|
|
174
|
+
const e = error;
|
|
175
|
+
throw new Error(`scdet pass failed: ${e.message}`);
|
|
176
|
+
}
|
|
177
|
+
return parseScdetOutput(stderr).map((sample) => ({
|
|
178
|
+
...sample,
|
|
179
|
+
t: sample.t + startSeconds,
|
|
180
|
+
}));
|
|
181
|
+
}
|
|
182
|
+
export async function refineCameraCandidate(videoPath, candidate, signal) {
|
|
183
|
+
const startSeconds = Math.max(0, candidate.t - 0.75);
|
|
184
|
+
const samples = await detectSceneSamples(videoPath, {
|
|
185
|
+
startSeconds,
|
|
186
|
+
durationSeconds: 1.5,
|
|
187
|
+
signal,
|
|
188
|
+
});
|
|
189
|
+
const best = samples
|
|
190
|
+
.filter((sample) => Math.abs(sample.t - candidate.t) <= 0.75)
|
|
191
|
+
.sort((left, right) => right.score - left.score)[0];
|
|
192
|
+
return best ? { t: best.t, score: best.score } : candidate;
|
|
193
|
+
}
|
|
194
|
+
export async function detectSceneCandidates(videoPath, signal) {
|
|
195
|
+
const samples = await detectSceneSamples(videoPath, {
|
|
196
|
+
fps: SCDET_FPS,
|
|
197
|
+
signal,
|
|
198
|
+
});
|
|
199
|
+
return deriveCandidates(samples);
|
|
200
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export const MIN_TRANSITION_SECONDS = 0.05;
|
|
2
|
+
export function fixedTransitionSeconds(firstDuration, secondDuration, requested) {
|
|
3
|
+
const available = Math.min(firstDuration, secondDuration);
|
|
4
|
+
const transition = requested ?? Math.max(MIN_TRANSITION_SECONDS, Math.min(0.35, available / 2));
|
|
5
|
+
if (!Number.isFinite(transition) || transition < MIN_TRANSITION_SECONDS) {
|
|
6
|
+
throw new Error(`Transition must be at least ${MIN_TRANSITION_SECONDS} seconds`);
|
|
7
|
+
}
|
|
8
|
+
if (!Number.isFinite(available) || transition >= available) {
|
|
9
|
+
throw new Error("Transition must be shorter than both video clips");
|
|
10
|
+
}
|
|
11
|
+
return transition;
|
|
12
|
+
}
|