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,124 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { findAudioFile, findPrimaryMediaFile } from "videobook-engine";
|
|
4
|
+
import { musicAnalysisSchema, sectionLabelsSchema } from "./model.js";
|
|
5
|
+
import { candidateSections } from "./energy.js";
|
|
6
|
+
import { extractWaveform, FFMPEG } from "../ffmpeg.js";
|
|
7
|
+
import { RHYTHM_ANALYZER_VERSION, RHYTHM_ANALYZER_ID, SECTION_LABELER_VERSION } from "./contract.js";
|
|
8
|
+
import { runRhythmWorker } from "./run-worker.js";
|
|
9
|
+
export async function musicSource(engine, artifactId, streamId) {
|
|
10
|
+
const artifact = engine.artifacts.get(artifactId);
|
|
11
|
+
if (!artifact.ok)
|
|
12
|
+
throw new Error(artifact.error.message);
|
|
13
|
+
if (!["audio", "video", "final"].includes(artifact.value.kind))
|
|
14
|
+
throw new Error("Choose an audio or video source");
|
|
15
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
16
|
+
if (!manifest.ok)
|
|
17
|
+
throw new Error(manifest.error.message);
|
|
18
|
+
const file = artifact.value.kind === "audio"
|
|
19
|
+
? findAudioFile(manifest.value.files, artifact.value.kind)
|
|
20
|
+
: findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
|
|
21
|
+
const streams = engine.streams.list(artifactId);
|
|
22
|
+
if (!streams.ok)
|
|
23
|
+
throw new Error(streams.error.message);
|
|
24
|
+
const stream = streams.value.find((item) => item.kind === "audio" && item.sourcePath === file?.name
|
|
25
|
+
&& item.objectHash === file.objectHash && (!streamId || item.streamId === streamId));
|
|
26
|
+
if (!stream || !file)
|
|
27
|
+
throw new Error("Wait for the current audio source to finish processing");
|
|
28
|
+
const durationSeconds = stream.durationTicks * stream.timeBase.numerator / stream.timeBase.denominator;
|
|
29
|
+
if (!(durationSeconds > 0) || durationSeconds > 3600)
|
|
30
|
+
throw new Error("Music analysis supports audio up to 60 minutes");
|
|
31
|
+
const analysisId = createHash("sha256").update(JSON.stringify([
|
|
32
|
+
file.objectHash, stream.streamIndex, RHYTHM_ANALYZER_ID, RHYTHM_ANALYZER_VERSION,
|
|
33
|
+
])).digest("hex");
|
|
34
|
+
return { artifactId, objectHash: file.objectHash, streamId: stream.streamId, streamIndex: stream.streamIndex,
|
|
35
|
+
analysisId, durationSeconds, inputPath: join(manifest.value.path, file.name) };
|
|
36
|
+
}
|
|
37
|
+
export async function cachedMusicAnalysis(engine, source) {
|
|
38
|
+
const read = await engine.metadata.artifacts.read(source.artifactId, `music.analysis.${source.analysisId}`);
|
|
39
|
+
if (!read.ok)
|
|
40
|
+
return null;
|
|
41
|
+
const parsed = musicAnalysisSchema.safeParse(read.value);
|
|
42
|
+
if (!parsed.success || parsed.data.objectHash !== source.objectHash || parsed.data.streamId !== source.streamId)
|
|
43
|
+
return null;
|
|
44
|
+
return parsed.data;
|
|
45
|
+
}
|
|
46
|
+
export async function analyzeMusicSource(engine, artifactId, analysisId, streamId, signal) {
|
|
47
|
+
const source = await musicSource(engine, artifactId, streamId);
|
|
48
|
+
if (source.analysisId !== analysisId)
|
|
49
|
+
throw new Error("The audio source changed; analyze it again");
|
|
50
|
+
const analysis = await runRhythmWorker(FFMPEG, source.inputPath, signal, source.streamIndex);
|
|
51
|
+
signal.throwIfAborted();
|
|
52
|
+
const waveform = await extractWaveform(source.inputPath, 8192, source.streamIndex, signal);
|
|
53
|
+
signal.throwIfAborted();
|
|
54
|
+
if (!waveform.length)
|
|
55
|
+
throw new Error("The soundtrack waveform could not be decoded. Retry analysis.");
|
|
56
|
+
const current = await musicSource(engine, artifactId, streamId);
|
|
57
|
+
if (current.analysisId !== analysisId)
|
|
58
|
+
throw new Error("The audio source changed during analysis");
|
|
59
|
+
const withinSource = (beat) => beat.timeSeconds < source.durationSeconds;
|
|
60
|
+
const energy = analysis.energy;
|
|
61
|
+
const result = musicAnalysisSchema.parse({
|
|
62
|
+
analysisId, artifactId, objectHash: source.objectHash, streamId, durationSeconds: source.durationSeconds,
|
|
63
|
+
analyzerVersion: `${RHYTHM_ANALYZER_ID}@${RHYTHM_ANALYZER_VERSION}`,
|
|
64
|
+
beats: analysis.beats.filter(withinSource), bpm: analysis.bpm, confidence: analysis.bpmConfidence,
|
|
65
|
+
bars: analysis.bars?.filter(withinSource) ?? null, beatsPerBar: analysis.meter?.beatsPerBar ?? null,
|
|
66
|
+
barSource: analysis.barSource,
|
|
67
|
+
waveform, lowConfidence: analysis.lowConfidence,
|
|
68
|
+
energy: energy ?? null,
|
|
69
|
+
sections: energy ? candidateSections(energy, source.durationSeconds) : null,
|
|
70
|
+
});
|
|
71
|
+
const saved = await engine.metadata.artifacts.write(artifactId, `music.analysis.${analysisId}`, result);
|
|
72
|
+
if (!saved.ok)
|
|
73
|
+
throw new Error(saved.error.message);
|
|
74
|
+
return result;
|
|
75
|
+
}
|
|
76
|
+
export async function readMusicAnalysis(engine, artifactId, streamId) {
|
|
77
|
+
const source = await musicSource(engine, artifactId, streamId);
|
|
78
|
+
const analysis = await cachedMusicAnalysis(engine, source);
|
|
79
|
+
const jobs = engine.jobs.queue.list({ type: "analyze_music", limit: 500 });
|
|
80
|
+
const job = jobs.filter((item) => item.payload.musicArtifactId === artifactId && item.payload.analysisId === source.analysisId)
|
|
81
|
+
.sort((left, right) => right.id - left.id)[0];
|
|
82
|
+
const pending = job && ["queued", "running", "completing"].includes(job.state);
|
|
83
|
+
const status = pending ? job.state === "queued" ? "queued" : "analyzing"
|
|
84
|
+
: job?.state === "aborted" ? "cancelled" : job?.state === "failed" ? "failed"
|
|
85
|
+
: analysis ? analysis.beats.length ? "ready" : "no-beats" : "missing";
|
|
86
|
+
const sections = await readSectionState(engine, source, analysis);
|
|
87
|
+
return { status, analysisId: source.analysisId, jobId: job?.id ?? null, error: job?.error?.message ?? null, analysis, sections };
|
|
88
|
+
}
|
|
89
|
+
async function readSectionState(engine, source, analysis) {
|
|
90
|
+
if (!analysis?.energy || !analysis.sections)
|
|
91
|
+
return { status: "missing", jobId: null, error: null, labels: null };
|
|
92
|
+
const stored = await readSectionLabels(engine, source.artifactId, source.analysisId);
|
|
93
|
+
if (stored && sectionLabelsCurrent(stored, analysis)) {
|
|
94
|
+
return {
|
|
95
|
+
status: stored.status === "ready" ? "ready" : "unavailable",
|
|
96
|
+
jobId: null, error: stored.status === "ready" ? null : stored.reason,
|
|
97
|
+
labels: stored.status === "ready" ? stored : null,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const jobs = engine.jobs.queue.list({ type: "label_music_sections", limit: 500 });
|
|
101
|
+
const job = jobs.filter((item) => item.payload.analysisId === source.analysisId).sort((left, right) => right.id - left.id)[0];
|
|
102
|
+
const active = job && ["queued", "running", "completing"].includes(job.state);
|
|
103
|
+
const status = active ? job.state === "queued" ? "queued" : "labeling"
|
|
104
|
+
: job?.state === "aborted" ? "cancelled" : job?.state === "failed" ? "failed" : "missing";
|
|
105
|
+
return { status, jobId: job?.id ?? null, error: job?.error?.message ?? null, labels: null };
|
|
106
|
+
}
|
|
107
|
+
export async function readSectionLabels(engine, artifactId, analysisId) {
|
|
108
|
+
const read = await engine.metadata.artifacts.read(artifactId, `music.sections.${analysisId}`);
|
|
109
|
+
if (!read.ok)
|
|
110
|
+
return null;
|
|
111
|
+
const parsed = sectionLabelsSchema.safeParse(read.value);
|
|
112
|
+
return parsed.success ? parsed.data : null;
|
|
113
|
+
}
|
|
114
|
+
export function sectionLabelsCurrent(stored, analysis) {
|
|
115
|
+
const candidates = analysis.sections?.candidates ?? [];
|
|
116
|
+
const covered = stored.status === "ready"
|
|
117
|
+
&& stored.labels.length === candidates.length
|
|
118
|
+
&& stored.labels.every((label, index) => label.candidateId === candidates[index]?.id);
|
|
119
|
+
return stored.analysisId === analysis.analysisId && stored.objectHash === analysis.objectHash
|
|
120
|
+
&& stored.streamId === analysis.streamId && stored.labeler.version === SECTION_LABELER_VERSION
|
|
121
|
+
&& stored.boundaries.algorithmVersion === analysis.sections?.algorithmVersion
|
|
122
|
+
&& stored.boundaries.algorithmId === analysis.sections?.algorithmId
|
|
123
|
+
&& (stored.status === "unavailable" || covered);
|
|
124
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { withNotebookMutation } from "../notebook-mutations.js";
|
|
2
|
+
import { v5 as uuidv5 } from "uuid";
|
|
3
|
+
import { findPrimaryMediaFile, MVP_CONTRACT_VERSION } from "videobook-engine";
|
|
4
|
+
import { sequenceContinuationRevision } from "../tools/command-plan.js";
|
|
5
|
+
import { MOMENTS_MAX_RANKED, readEditingMoments } from "../editing-moments.js";
|
|
6
|
+
import { cachedMusicAnalysis, musicSource } from "./music-analysis.js";
|
|
7
|
+
import { readSequenceRhythm } from "./sequence-rhythm.js";
|
|
8
|
+
import { effectiveMarkers, frameAtSourceTime, soundtrackClip, timelineMarkers } from "./timing.js";
|
|
9
|
+
import { musicDraftOptionsSchema } from "./draft-model.js";
|
|
10
|
+
import { energySegments } from "./energy.js";
|
|
11
|
+
import { fittedTransform } from "./visual-fit.js";
|
|
12
|
+
const namespace = "5cc43cf4-f4bf-4dce-aa1e-a8105ae53087";
|
|
13
|
+
const endOf = (clip) => clip.timelineStartFrame + clip.durationFrames;
|
|
14
|
+
export async function planMusicVideo(engine, input, expectedSequence, expectedRhythm, actor, sourceSurface) {
|
|
15
|
+
const options = musicDraftOptionsSchema.parse(input);
|
|
16
|
+
const sequence = engine.sequences.getPrimary();
|
|
17
|
+
const contentRevision = sequenceContinuationRevision(sequence);
|
|
18
|
+
if (contentRevision !== expectedSequence && sequence.revision !== expectedSequence)
|
|
19
|
+
throw new Error("The timeline changed. Reload before making a draft.");
|
|
20
|
+
const rhythm = await readSequenceRhythm(engine, sequence.sequenceId);
|
|
21
|
+
if (rhythm.revision !== expectedRhythm)
|
|
22
|
+
throw new Error("The beat map changed. Reload before making a draft.");
|
|
23
|
+
const master = soundtrackClip(sequence, rhythm.settings);
|
|
24
|
+
if (!master)
|
|
25
|
+
throw new Error("Choose a master soundtrack before making a draft.");
|
|
26
|
+
const source = await musicSource(engine, master.source.artifactId, master.source.range.streamId);
|
|
27
|
+
if (source.objectHash !== master.source.range.objectHash)
|
|
28
|
+
throw new Error("The soundtrack source changed. Replace the timeline clip first.");
|
|
29
|
+
const analysis = await cachedMusicAnalysis(engine, source);
|
|
30
|
+
if (!rhythm.settings.correction && (!analysis || analysis.lowConfidence))
|
|
31
|
+
throw new Error("Analyze the song and confirm uncertain beats with a manual grid before making a draft.");
|
|
32
|
+
if (options.analysis && (!analysis || analysis.analysisId !== options.analysis.music_analysis_id)) {
|
|
33
|
+
throw new Error("The referenced music analysis is stale; analyze the current soundtrack before planning with it.");
|
|
34
|
+
}
|
|
35
|
+
const moments = await resolveMoments(engine, options);
|
|
36
|
+
const markers = timelineMarkers(master, effectiveMarkers(master, rhythm.settings, analysis), sequence);
|
|
37
|
+
const musical = markers.filter((marker) => options.unit === "beats" || marker.bar);
|
|
38
|
+
if (musical.length < 2)
|
|
39
|
+
throw new Error(options.unit === "bars" ? "Confirm bar starts in Soundtrack before using bars." : "Add at least two beat markers before making a draft.");
|
|
40
|
+
const track = sequence.tracks.find((item) => item.trackId === options.track_id);
|
|
41
|
+
if (!track || track.kind !== "video" || !track.enabled || track.locked)
|
|
42
|
+
throw new Error("Choose an enabled, unlocked video track.");
|
|
43
|
+
if (options.start_frame >= options.end_frame || options.start_frame < master.timelineStartFrame || options.end_frame > endOf(master))
|
|
44
|
+
throw new Error("Choose a nonempty range inside the master soundtrack.");
|
|
45
|
+
const touching = sequence.clips.filter((clip) => clip.trackId === track.trackId && clip.timelineStartFrame < options.end_frame && endOf(clip) > options.start_frame);
|
|
46
|
+
const preserved = touching.filter((clip) => options.preserve_existing || options.preserve_clip_ids.includes(clip.clipId) || clip.timelineStartFrame < options.start_frame || endOf(clip) > options.end_frame);
|
|
47
|
+
const removed = touching.filter((clip) => !preserved.includes(clip));
|
|
48
|
+
if (sequence.transitions.some((transition) => removed.some((clip) => transition.outgoingClipId === clip.clipId || transition.incomingClipId === clip.clipId)))
|
|
49
|
+
throw new Error("Remove transitions in the replacement range or keep the existing clips.");
|
|
50
|
+
const gaps = [];
|
|
51
|
+
let cursor = options.start_frame;
|
|
52
|
+
for (const clip of [...preserved].sort((a, b) => a.timelineStartFrame - b.timelineStartFrame)) {
|
|
53
|
+
if (clip.timelineStartFrame > cursor)
|
|
54
|
+
gaps.push({ start: cursor, end: Math.min(clip.timelineStartFrame, options.end_frame) });
|
|
55
|
+
cursor = Math.max(cursor, endOf(clip));
|
|
56
|
+
}
|
|
57
|
+
if (cursor < options.end_frame)
|
|
58
|
+
gaps.push({ start: cursor, end: options.end_frame });
|
|
59
|
+
if (!gaps.length)
|
|
60
|
+
throw new Error("This range is already filled. Turn off Keep existing clips to replace it, or choose an empty range.");
|
|
61
|
+
const fps = sequence.frameRate.numerator / sequence.frameRate.denominator;
|
|
62
|
+
const sources = await Promise.all(options.sources.map(async (selection) => {
|
|
63
|
+
const artifact = engine.artifacts.get(selection.artifact_id);
|
|
64
|
+
if (!artifact.ok || !["video", "final"].includes(artifact.value.kind))
|
|
65
|
+
throw new Error("Choose ready video sources.");
|
|
66
|
+
const manifest = await engine.files.manifest(selection.artifact_id);
|
|
67
|
+
const streams = engine.streams.list(selection.artifact_id);
|
|
68
|
+
if (!manifest.ok || !streams.ok)
|
|
69
|
+
throw new Error("A source is unavailable. Refresh media.");
|
|
70
|
+
const file = findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
|
|
71
|
+
const stream = streams.value.find((item) => item.kind === "video" && item.objectHash === file?.objectHash && item.sourcePath === file.name);
|
|
72
|
+
if (!stream)
|
|
73
|
+
throw new Error(`Wait for ${artifact.value.label} to finish processing.`);
|
|
74
|
+
const tickSeconds = stream.timeBase.numerator / stream.timeBase.denominator;
|
|
75
|
+
const first = Math.round((selection.start_seconds ?? 0) / tickSeconds);
|
|
76
|
+
const last = selection.end_seconds === undefined ? stream.durationTicks : Math.round(selection.end_seconds / tickSeconds);
|
|
77
|
+
if (first < 0 || last > stream.durationTicks || first >= last)
|
|
78
|
+
throw new Error(`Source range is outside ${artifact.value.label}.`);
|
|
79
|
+
return { artifactId: selection.artifact_id, stream, tickSeconds, first, last, used: first };
|
|
80
|
+
}));
|
|
81
|
+
if (new Set(sources.map((item) => item.artifactId)).size !== sources.length)
|
|
82
|
+
throw new Error("Select each source once; enable source reuse to repeat footage.");
|
|
83
|
+
const snapshot = engine.head;
|
|
84
|
+
const currentRhythm = await readSequenceRhythm(engine, sequence.sequenceId);
|
|
85
|
+
if (snapshot !== engine.head || currentRhythm.revision !== rhythm.revision || sequenceContinuationRevision(engine.sequences.getPrimary()) !== contentRevision)
|
|
86
|
+
throw new Error("The timeline or beat map changed while planning. Try again.");
|
|
87
|
+
const commandId = uuidv5(JSON.stringify([snapshot, rhythm.revision, options, sources.map((item) => item.stream.objectHash)]), namespace);
|
|
88
|
+
const operations = removed.map((clip) => ({ kind: "remove-clip", clipId: clip.clipId, ripple: false }));
|
|
89
|
+
const inserted = [];
|
|
90
|
+
const shots = [];
|
|
91
|
+
const uncovered = [];
|
|
92
|
+
const warnings = new Set();
|
|
93
|
+
let sourceIndex = 0;
|
|
94
|
+
const energy = options.strategy === "follow_song_energy"
|
|
95
|
+
? analysis?.energy?.values.length
|
|
96
|
+
? { segments: energySegments(analysis.energy, analysis.sections, analysis.durationSeconds) }
|
|
97
|
+
: null
|
|
98
|
+
: null;
|
|
99
|
+
if (options.strategy === "follow_song_energy" && !energy) {
|
|
100
|
+
throw new Error("Follow-song-energy planning needs an analysis reference with an energy curve; run analyze_music on the current soundtrack first.");
|
|
101
|
+
}
|
|
102
|
+
if (energy && !analysis?.sections?.candidates.length)
|
|
103
|
+
warnings.add("The analysis has no section boundaries; the whole range keeps your base interval.");
|
|
104
|
+
const intervalFrames = energy
|
|
105
|
+
? energyCutFrames(musical, energy.segments, options.interval)
|
|
106
|
+
: musical.filter((_, index) => index % options.interval === 0).map((marker) => marker.frame);
|
|
107
|
+
for (const gap of gaps) {
|
|
108
|
+
const boundaries = [gap.start];
|
|
109
|
+
for (const frame of intervalFrames) {
|
|
110
|
+
if (frame - boundaries[boundaries.length - 1] >= options.minimum_shot_frames && gap.end - frame >= options.minimum_shot_frames)
|
|
111
|
+
boundaries.push(frame);
|
|
112
|
+
}
|
|
113
|
+
boundaries.push(gap.end);
|
|
114
|
+
for (let index = 1; index < boundaries.length; index++) {
|
|
115
|
+
if (shots.length >= 1000)
|
|
116
|
+
throw new Error("Limit the draft to 1,000 shots; choose a longer interval or shorter range.");
|
|
117
|
+
const start = boundaries[index - 1];
|
|
118
|
+
const end = boundaries[index];
|
|
119
|
+
const seconds = (end - start) / fps;
|
|
120
|
+
let chosen;
|
|
121
|
+
let ticks = 0;
|
|
122
|
+
let retimed = false;
|
|
123
|
+
for (let attempt = 0; attempt < sources.length; attempt++) {
|
|
124
|
+
const candidate = sources[(sourceIndex + attempt) % sources.length];
|
|
125
|
+
const required = Math.ceil(seconds / candidate.tickSeconds - 1e-7);
|
|
126
|
+
if (candidate.last - candidate.used >= required) {
|
|
127
|
+
chosen = candidate;
|
|
128
|
+
ticks = required;
|
|
129
|
+
sourceIndex = (sourceIndex + attempt + 1) % sources.length;
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
if (options.reuse_sources && candidate.last - candidate.first >= required) {
|
|
133
|
+
chosen = candidate;
|
|
134
|
+
chosen.used = chosen.first;
|
|
135
|
+
ticks = required;
|
|
136
|
+
sourceIndex = (sourceIndex + attempt + 1) % sources.length;
|
|
137
|
+
warnings.add("Some source footage is repeated.");
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!chosen && options.allow_retiming) {
|
|
142
|
+
const candidate = sources[sourceIndex % sources.length];
|
|
143
|
+
if (candidate.used === candidate.last && options.reuse_sources) {
|
|
144
|
+
candidate.used = candidate.first;
|
|
145
|
+
warnings.add("Some source footage is repeated.");
|
|
146
|
+
}
|
|
147
|
+
if (candidate.used < candidate.last) {
|
|
148
|
+
chosen = candidate;
|
|
149
|
+
ticks = candidate.last - candidate.used;
|
|
150
|
+
retimed = true;
|
|
151
|
+
sourceIndex = (sourceIndex + 1) % sources.length;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
if (!chosen) {
|
|
155
|
+
uncovered.push({ startFrame: start, endFrame: end });
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
const clipId = uuidv5(`shot:${shots.length}`, commandId);
|
|
159
|
+
const nativeFrames = ticks * chosen.tickSeconds * fps;
|
|
160
|
+
const speed = retimed ? { numerator: Math.max(1, Math.round(nativeFrames * 1000)), denominator: (end - start) * 1000 } : { numerator: 1, denominator: 1 };
|
|
161
|
+
const clip = {
|
|
162
|
+
clipId, trackId: track.trackId, timelineStartFrame: start, durationFrames: end - start, enabled: true,
|
|
163
|
+
source: { kind: "timed", artifactId: chosen.artifactId, range: { streamId: chosen.stream.streamId, objectHash: chosen.stream.objectHash, timeBase: chosen.stream.timeBase, startTick: chosen.used, durationTicks: ticks } },
|
|
164
|
+
speed, reverse: false, audioPolicy: "mute", transform: fittedTransform(),
|
|
165
|
+
};
|
|
166
|
+
const { clipId: _id, enabled: _enabled, ...placement } = clip;
|
|
167
|
+
operations.push({ kind: "insert-clip", clipId, mode: "overwrite", placement });
|
|
168
|
+
inserted.push(clip);
|
|
169
|
+
shots.push({ clipId, artifactId: chosen.artifactId, startFrame: start, endFrame: end, sourceStartSeconds: chosen.used * chosen.tickSeconds, sourceEndSeconds: (chosen.used + ticks) * chosen.tickSeconds, speed: speed.numerator / speed.denominator });
|
|
170
|
+
chosen.used += ticks;
|
|
171
|
+
if (retimed)
|
|
172
|
+
warnings.add("Some shots change playback speed. Review each rate below.");
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (!shots.length)
|
|
176
|
+
throw new Error("No source is long enough for this interval. Choose shorter cuts, more footage, or explicitly allow retiming.");
|
|
177
|
+
if (uncovered.length)
|
|
178
|
+
warnings.add(`${uncovered.length} spans have no footage. Add footage, shorten the interval, or enable reuse before applying.`);
|
|
179
|
+
if (!markers.some((marker) => marker.frame === options.end_frame))
|
|
180
|
+
warnings.add("The range ends between beats; the last shot ends exactly at your selected out point.");
|
|
181
|
+
let energyPlan;
|
|
182
|
+
if (energy && analysis) {
|
|
183
|
+
const segments = [];
|
|
184
|
+
for (const segment of energy.segments) {
|
|
185
|
+
const start = Math.max(options.start_frame, frameAtSourceTime(master, segment.startSeconds, fps));
|
|
186
|
+
const end = Math.min(options.end_frame, frameAtSourceTime(master, segment.endSeconds, fps));
|
|
187
|
+
if (end <= start)
|
|
188
|
+
continue;
|
|
189
|
+
segments.push({ startFrame: start, endFrame: end, tier: segment.tier, interval: segmentInterval(segment.tier, options.interval, energy.segments.length) });
|
|
190
|
+
}
|
|
191
|
+
energyPlan = { strategy: "follow_song_energy", musicAnalysisId: analysis.analysisId, segments };
|
|
192
|
+
}
|
|
193
|
+
const intent = { intentVersion: MVP_CONTRACT_VERSION, commandId, sequenceId: sequence.sequenceId, baseRevision: snapshot, actor, sourceSurface, confirmationPolicy: "always", operations };
|
|
194
|
+
const preview = engine.edits.preview(intent);
|
|
195
|
+
if (!preview.ok)
|
|
196
|
+
throw new Error(preview.error.message);
|
|
197
|
+
if (!preview.value.valid)
|
|
198
|
+
throw new Error(preview.value.conflicts.map((conflict) => conflict.message).join("; "));
|
|
199
|
+
const summary = `${shots.length} shots across ${((options.end_frame - options.start_frame) / fps).toFixed(2)}s`;
|
|
200
|
+
return {
|
|
201
|
+
plan: { planVersion: 1, planId: commandId, sourceSurface, actor, bookId: engine.book.get().bookId, summary,
|
|
202
|
+
normalizedReferences: [{ kind: "sequence", targetId: sequence.sequenceId, revision: snapshot }, ...sources.map((item) => ({ kind: "artifact", targetId: item.artifactId, objectHash: item.stream.objectHash }))],
|
|
203
|
+
stages: [{ stageId: `${commandId}:edit:0`, kind: "edit", summary, intent }], confirmation: { required: true, policy: "always", reason: "Review the music-video draft before replacing footage" } },
|
|
204
|
+
preview: preview.value, sequence: { ...sequence, clips: [...sequence.clips.filter((clip) => !removed.includes(clip)), ...inserted] },
|
|
205
|
+
rhythmRevision: rhythm.revision, sequenceContentRevision: contentRevision, shots, uncovered, warnings: [...warnings, ...preview.value.warnings.map((warning) => warning.message)],
|
|
206
|
+
...(energyPlan ? { energyPlan } : {}),
|
|
207
|
+
...(moments.length ? { sourceMoments: moments } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function segmentInterval(tier, base, segmentCount) {
|
|
211
|
+
if (segmentCount <= 1)
|
|
212
|
+
return base;
|
|
213
|
+
const multiplier = tier === "high" ? 1 : tier === "mid" ? 2 : 4;
|
|
214
|
+
return Math.min(8, base * multiplier);
|
|
215
|
+
}
|
|
216
|
+
function energyCutFrames(markers, segments, base) {
|
|
217
|
+
const selected = [];
|
|
218
|
+
for (const segment of segments) {
|
|
219
|
+
const inside = markers.filter((marker) => marker.timeSeconds >= segment.startSeconds && marker.timeSeconds < segment.endSeconds);
|
|
220
|
+
const interval = segmentInterval(segment.tier, base, segments.length);
|
|
221
|
+
inside.forEach((marker, index) => {
|
|
222
|
+
if (index % interval === 0)
|
|
223
|
+
selected.push(marker.frame);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
return [...new Set(selected)].sort((left, right) => left - right);
|
|
227
|
+
}
|
|
228
|
+
async function resolveMoments(engine, options) {
|
|
229
|
+
const ranked = [];
|
|
230
|
+
for (const ref of options.analysis?.moments ?? []) {
|
|
231
|
+
const doc = await readEditingMoments(engine, ref.artifact_id, ref.moments_id);
|
|
232
|
+
if (!doc)
|
|
233
|
+
throw new Error("A referenced moments analysis is stale or missing; analyze that footage again before planning with it.");
|
|
234
|
+
for (const moment of doc.moments) {
|
|
235
|
+
ranked.push({ artifactId: doc.artifactId, shotId: moment.shotId, startSeconds: moment.startSeconds, endSeconds: moment.endSeconds, score: moment.score, quality: moment.quality });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return ranked
|
|
239
|
+
.sort((left, right) => right.score - left.score || left.artifactId.localeCompare(right.artifactId) || left.startSeconds - right.startSeconds)
|
|
240
|
+
.slice(0, MOMENTS_MAX_RANKED);
|
|
241
|
+
}
|
|
242
|
+
export async function commitMusicVideo(engine, intent, previewHash, rhythmRevision, sequenceContentRevision) {
|
|
243
|
+
return withNotebookMutation(`rhythm:${engine.book.get().bookId}:${intent.sequenceId}`, async () => {
|
|
244
|
+
const previous = engine.history.revisions(Number.MAX_SAFE_INTEGER).find((revision) => revision.operation === "commit_edit" && revision.details?.commandId === intent.commandId);
|
|
245
|
+
if (previous) {
|
|
246
|
+
if (previous.details?.previewHash !== previewHash)
|
|
247
|
+
throw new Error("This draft ID was already applied with a different preview.");
|
|
248
|
+
return { commandId: intent.commandId, actionId: String(previous.details.actionId), revision: engine.head,
|
|
249
|
+
sequence: engine.sequences.getPrimary(), previewHash };
|
|
250
|
+
}
|
|
251
|
+
const rhythm = await readSequenceRhythm(engine, intent.sequenceId);
|
|
252
|
+
if (rhythm.revision !== rhythmRevision)
|
|
253
|
+
throw new Error("The beat map changed. Preview a new draft before applying.");
|
|
254
|
+
if (sequenceContinuationRevision(engine.sequences.getPrimary()) !== sequenceContentRevision)
|
|
255
|
+
throw new Error("The timeline changed. Preview a new draft before applying.");
|
|
256
|
+
const committed = await engine.edits.commit(intent, previewHash);
|
|
257
|
+
if (!committed.ok)
|
|
258
|
+
throw new Error(committed.error.message);
|
|
259
|
+
return committed.value;
|
|
260
|
+
});
|
|
261
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { fftInPlace } from "./fft.js";
|
|
2
|
+
import { RHYTHM_HOP_SAMPLES, RHYTHM_LATENCY_SAMPLES, RHYTHM_MAX_RETAINED_SAMPLES, RHYTHM_SAMPLE_RATE, RHYTHM_WINDOW_SAMPLES, } from "./contract.js";
|
|
3
|
+
const HANN = new Float64Array(RHYTHM_WINDOW_SAMPLES);
|
|
4
|
+
for (let index = 0; index < RHYTHM_WINDOW_SAMPLES; index += 1) {
|
|
5
|
+
HANN[index] = 0.5 * (1 - Math.cos((2 * Math.PI * index) / (RHYTHM_WINDOW_SAMPLES - 1)));
|
|
6
|
+
}
|
|
7
|
+
export function createOnsetAccumulator(sampleRate = RHYTHM_SAMPLE_RATE) {
|
|
8
|
+
if (sampleRate !== RHYTHM_SAMPLE_RATE) {
|
|
9
|
+
throw new Error(`Rhythm analysis requires ${RHYTHM_SAMPLE_RATE} Hz mono PCM`);
|
|
10
|
+
}
|
|
11
|
+
const window = new Float32Array(RHYTHM_WINDOW_SAMPLES);
|
|
12
|
+
let filled = RHYTHM_LATENCY_SAMPLES;
|
|
13
|
+
const previous = new Float64Array(RHYTHM_WINDOW_SAMPLES / 2);
|
|
14
|
+
const real = new Float64Array(RHYTHM_WINDOW_SAMPLES);
|
|
15
|
+
const imag = new Float64Array(RHYTHM_WINDOW_SAMPLES);
|
|
16
|
+
const strength = [];
|
|
17
|
+
const rms = [];
|
|
18
|
+
let incoming = new Float32Array(0);
|
|
19
|
+
const emitFrame = () => {
|
|
20
|
+
let squares = 0;
|
|
21
|
+
for (let index = 0; index < RHYTHM_WINDOW_SAMPLES; index += 1)
|
|
22
|
+
squares += window[index] * window[index];
|
|
23
|
+
rms.push(Math.sqrt(squares / RHYTHM_WINDOW_SAMPLES));
|
|
24
|
+
for (let index = 0; index < RHYTHM_WINDOW_SAMPLES; index += 1) {
|
|
25
|
+
real[index] = window[index] * HANN[index];
|
|
26
|
+
imag[index] = 0;
|
|
27
|
+
}
|
|
28
|
+
fftInPlace(real, imag);
|
|
29
|
+
let flux = 0;
|
|
30
|
+
const bins = RHYTHM_WINDOW_SAMPLES / 2;
|
|
31
|
+
for (let bin = 1; bin < bins; bin += 1) {
|
|
32
|
+
const magnitude = Math.hypot(real[bin], imag[bin]);
|
|
33
|
+
const rise = magnitude - previous[bin];
|
|
34
|
+
if (rise > 0)
|
|
35
|
+
flux += rise;
|
|
36
|
+
previous[bin] = magnitude;
|
|
37
|
+
}
|
|
38
|
+
strength.push(flux);
|
|
39
|
+
window.copyWithin(0, RHYTHM_HOP_SAMPLES);
|
|
40
|
+
filled = RHYTHM_WINDOW_SAMPLES - RHYTHM_HOP_SAMPLES;
|
|
41
|
+
};
|
|
42
|
+
const drain = () => {
|
|
43
|
+
let offset = 0;
|
|
44
|
+
while (offset < incoming.length) {
|
|
45
|
+
const needed = RHYTHM_WINDOW_SAMPLES - filled;
|
|
46
|
+
const take = Math.min(needed, incoming.length - offset);
|
|
47
|
+
window.set(incoming.subarray(offset, offset + take), filled);
|
|
48
|
+
filled += take;
|
|
49
|
+
offset += take;
|
|
50
|
+
if (filled === RHYTHM_WINDOW_SAMPLES)
|
|
51
|
+
emitFrame();
|
|
52
|
+
}
|
|
53
|
+
incoming = new Float32Array(0);
|
|
54
|
+
};
|
|
55
|
+
return {
|
|
56
|
+
push(chunk) {
|
|
57
|
+
incoming = chunk;
|
|
58
|
+
drain();
|
|
59
|
+
},
|
|
60
|
+
retainedSamples() {
|
|
61
|
+
return filled + incoming.length;
|
|
62
|
+
},
|
|
63
|
+
hopCount() {
|
|
64
|
+
return strength.length;
|
|
65
|
+
},
|
|
66
|
+
finish() {
|
|
67
|
+
const raw = Float32Array.from(strength);
|
|
68
|
+
whitenFlux(raw);
|
|
69
|
+
return {
|
|
70
|
+
sampleRate,
|
|
71
|
+
hopSamples: RHYTHM_HOP_SAMPLES,
|
|
72
|
+
windowSamples: RHYTHM_WINDOW_SAMPLES,
|
|
73
|
+
strength: raw,
|
|
74
|
+
rms: Float32Array.from(rms),
|
|
75
|
+
frameCount: raw.length,
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
export function onsetRetainedLimit() {
|
|
81
|
+
return RHYTHM_MAX_RETAINED_SAMPLES;
|
|
82
|
+
}
|
|
83
|
+
function whitenFlux(strength) {
|
|
84
|
+
const width = 8;
|
|
85
|
+
const local = new Float32Array(strength.length);
|
|
86
|
+
for (let index = 0; index < strength.length; index += 1) {
|
|
87
|
+
const start = Math.max(0, index - width);
|
|
88
|
+
const end = Math.min(strength.length, index + width + 1);
|
|
89
|
+
let sum = 0;
|
|
90
|
+
for (let cursor = start; cursor < end; cursor += 1)
|
|
91
|
+
sum += strength[cursor];
|
|
92
|
+
local[index] = Math.max(0, strength[index] - sum / (end - start));
|
|
93
|
+
}
|
|
94
|
+
let peak = 0;
|
|
95
|
+
for (const value of local)
|
|
96
|
+
peak = Math.max(peak, value);
|
|
97
|
+
const scale = peak > 0 ? 1 / peak : 1;
|
|
98
|
+
for (let index = 0; index < strength.length; index += 1) {
|
|
99
|
+
strength[index] = local[index] * scale;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { fork } from "node:child_process";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
export async function runRhythmWorker(ffmpegPath, inputPath, signal, streamIndex) {
|
|
4
|
+
signal.throwIfAborted();
|
|
5
|
+
const development = import.meta.url.endsWith(".ts");
|
|
6
|
+
const child = fork(fileURLToPath(new URL(development ? "./worker.ts" : "./worker.js", import.meta.url)), [], {
|
|
7
|
+
execArgv: development ? ["--import", "tsx"] : [],
|
|
8
|
+
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
9
|
+
});
|
|
10
|
+
return new Promise((resolve, reject) => {
|
|
11
|
+
let settled = false;
|
|
12
|
+
let stderr = "";
|
|
13
|
+
let killTimer;
|
|
14
|
+
const finish = (error, result) => {
|
|
15
|
+
if (settled)
|
|
16
|
+
return;
|
|
17
|
+
settled = true;
|
|
18
|
+
signal.removeEventListener("abort", abort);
|
|
19
|
+
if (error)
|
|
20
|
+
reject(error);
|
|
21
|
+
else if (result)
|
|
22
|
+
resolve(result);
|
|
23
|
+
};
|
|
24
|
+
const abort = () => {
|
|
25
|
+
child.kill("SIGTERM");
|
|
26
|
+
killTimer = setTimeout(() => child.kill("SIGKILL"), 2000);
|
|
27
|
+
killTimer.unref();
|
|
28
|
+
finish(new Error("Music analysis cancelled"));
|
|
29
|
+
};
|
|
30
|
+
child.stderr?.on("data", (chunk) => { stderr = (stderr + chunk.toString()).slice(-8192); });
|
|
31
|
+
child.on("error", (error) => finish(error));
|
|
32
|
+
child.on("exit", (code) => {
|
|
33
|
+
if (killTimer)
|
|
34
|
+
clearTimeout(killTimer);
|
|
35
|
+
finish(new Error(`Music analysis stopped (${code}): ${stderr}`));
|
|
36
|
+
});
|
|
37
|
+
child.on("message", (message) => {
|
|
38
|
+
if (signal.aborted)
|
|
39
|
+
return;
|
|
40
|
+
if (message.analysis)
|
|
41
|
+
finish(undefined, message.analysis);
|
|
42
|
+
else
|
|
43
|
+
finish(new Error(message.error ?? "Music analyzer returned no result"));
|
|
44
|
+
});
|
|
45
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
46
|
+
if (signal.aborted)
|
|
47
|
+
abort();
|
|
48
|
+
else
|
|
49
|
+
child.send({ ffmpegPath, inputPath, streamIndex });
|
|
50
|
+
});
|
|
51
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
export function scoreBeats(referenceSeconds, detectedSeconds, toleranceSeconds) {
|
|
2
|
+
const used = new Set();
|
|
3
|
+
const errors = [];
|
|
4
|
+
for (const reference of referenceSeconds) {
|
|
5
|
+
let best = -1;
|
|
6
|
+
let bestDistance = Number.POSITIVE_INFINITY;
|
|
7
|
+
detectedSeconds.forEach((detected, index) => {
|
|
8
|
+
if (used.has(index))
|
|
9
|
+
return;
|
|
10
|
+
const distance = Math.abs(detected - reference);
|
|
11
|
+
if (distance <= toleranceSeconds && distance < bestDistance) {
|
|
12
|
+
best = index;
|
|
13
|
+
bestDistance = distance;
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
if (best >= 0) {
|
|
17
|
+
used.add(best);
|
|
18
|
+
errors.push(bestDistance);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const hits = errors.length;
|
|
22
|
+
const precision = detectedSeconds.length === 0
|
|
23
|
+
? (referenceSeconds.length === 0 ? 1 : 0)
|
|
24
|
+
: hits / detectedSeconds.length;
|
|
25
|
+
const recall = referenceSeconds.length === 0
|
|
26
|
+
? (detectedSeconds.length === 0 ? 1 : 0)
|
|
27
|
+
: hits / referenceSeconds.length;
|
|
28
|
+
const f1 = precision + recall === 0 ? 0 : (2 * precision * recall) / (precision + recall);
|
|
29
|
+
return {
|
|
30
|
+
precision,
|
|
31
|
+
recall,
|
|
32
|
+
f1,
|
|
33
|
+
medianAbsErrorSeconds: median(errors),
|
|
34
|
+
hits,
|
|
35
|
+
referenceCount: referenceSeconds.length,
|
|
36
|
+
detectedCount: detectedSeconds.length,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function median(values) {
|
|
40
|
+
if (values.length === 0)
|
|
41
|
+
return null;
|
|
42
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
43
|
+
const middle = Math.floor(sorted.length / 2);
|
|
44
|
+
return sorted.length % 2 === 0
|
|
45
|
+
? ((sorted[middle - 1] ?? 0) + (sorted[middle] ?? 0)) / 2
|
|
46
|
+
: sorted[middle] ?? 0;
|
|
47
|
+
}
|