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,1680 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { findAudioFile, findPrimaryMediaFile, MVP_CONTRACT_VERSION, sequenceFramesToSourceTicks, sourceTicksToSequenceFrames, } from "videobook-engine";
|
|
3
|
+
import { v7 as uuidv7 } from "uuid";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { scanNotebookMentions } from "videobook-engine/notebook-mentions";
|
|
6
|
+
import { fittedTransform } from "../rhythm/visual-fit.js";
|
|
7
|
+
import { resolveArtifactRefs } from "../artifact-refs.js";
|
|
8
|
+
const bookSchema = { book_id: z.string().uuid() };
|
|
9
|
+
export function sequenceContinuationRevision(sequence) {
|
|
10
|
+
const { revision: _revision, ...content } = sequence;
|
|
11
|
+
return createHash("sha256").update(JSON.stringify(content)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
const intentSchema = z.record(z.string(), z.unknown());
|
|
14
|
+
const editRequestSchema = z.discriminatedUnion("kind", [
|
|
15
|
+
z.object({
|
|
16
|
+
kind: z.literal("append"),
|
|
17
|
+
artifact_id: z.string().uuid(),
|
|
18
|
+
track_id: z.string().uuid(),
|
|
19
|
+
}),
|
|
20
|
+
z.object({
|
|
21
|
+
kind: z.literal("remove_clip"),
|
|
22
|
+
clip_id: z.string().uuid(),
|
|
23
|
+
ripple: z.boolean(),
|
|
24
|
+
}),
|
|
25
|
+
z.object({
|
|
26
|
+
kind: z.literal("insert"),
|
|
27
|
+
artifact_id: z.string().uuid(),
|
|
28
|
+
track_id: z.string().uuid().optional(),
|
|
29
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
30
|
+
duration_frames: z.number().int().positive().optional(),
|
|
31
|
+
source_start_tick: z.number().int().nonnegative().optional(),
|
|
32
|
+
source_duration_ticks: z.number().int().positive().optional(),
|
|
33
|
+
mode: z.enum(["insert", "overwrite"]).optional(),
|
|
34
|
+
}),
|
|
35
|
+
z.object({
|
|
36
|
+
kind: z.literal("trim"),
|
|
37
|
+
clip_id: z.string().uuid(),
|
|
38
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
39
|
+
duration_frames: z.number().int().positive(),
|
|
40
|
+
edge: z.enum(["start", "end"]).optional(),
|
|
41
|
+
ripple: z.boolean().optional(),
|
|
42
|
+
timeline_limit_frame: z.number().int().positive().optional(),
|
|
43
|
+
}),
|
|
44
|
+
z.object({
|
|
45
|
+
kind: z.literal("volume"),
|
|
46
|
+
clip_id: z.string().uuid(),
|
|
47
|
+
gain_db: z.number().min(-96).max(24),
|
|
48
|
+
muted: z.boolean().optional(),
|
|
49
|
+
fade_in_frames: z.number().int().nonnegative().optional(),
|
|
50
|
+
fade_out_frames: z.number().int().nonnegative().optional(),
|
|
51
|
+
}),
|
|
52
|
+
z.object({
|
|
53
|
+
kind: z.literal("duck"),
|
|
54
|
+
narration_clip_id: z.string().uuid(),
|
|
55
|
+
reduction_db: z.number().min(-36).max(-1),
|
|
56
|
+
}),
|
|
57
|
+
z.object({
|
|
58
|
+
kind: z.literal("move"),
|
|
59
|
+
clip_id: z.string().uuid(),
|
|
60
|
+
track_id: z.string().uuid(),
|
|
61
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
62
|
+
}),
|
|
63
|
+
z.object({
|
|
64
|
+
kind: z.literal("split"),
|
|
65
|
+
clip_id: z.string().uuid(),
|
|
66
|
+
split_frame: z.number().int().nonnegative(),
|
|
67
|
+
}),
|
|
68
|
+
z.object({
|
|
69
|
+
kind: z.literal("delete"),
|
|
70
|
+
clip_id: z.string().uuid(),
|
|
71
|
+
ripple: z.boolean(),
|
|
72
|
+
}),
|
|
73
|
+
z.object({
|
|
74
|
+
kind: z.literal("gap"),
|
|
75
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
76
|
+
duration_frames: z.number().int().positive(),
|
|
77
|
+
track_ids: z.array(z.string().uuid()).min(1).optional(),
|
|
78
|
+
}),
|
|
79
|
+
z.object({
|
|
80
|
+
kind: z.literal("transform"),
|
|
81
|
+
clip_id: z.string().uuid(),
|
|
82
|
+
fit: z.enum(["fit", "fill", "crop"]).optional(),
|
|
83
|
+
position_x: z.number().optional(),
|
|
84
|
+
position_y: z.number().optional(),
|
|
85
|
+
scale_x: z.number().positive().optional(),
|
|
86
|
+
scale_y: z.number().positive().optional(),
|
|
87
|
+
rotation_degrees: z.number().optional(),
|
|
88
|
+
opacity: z.number().min(0).max(1).optional(),
|
|
89
|
+
crop_top: z.number().min(0).max(1).optional(),
|
|
90
|
+
crop_right: z.number().min(0).max(1).optional(),
|
|
91
|
+
crop_bottom: z.number().min(0).max(1).optional(),
|
|
92
|
+
crop_left: z.number().min(0).max(1).optional(),
|
|
93
|
+
}),
|
|
94
|
+
z.object({
|
|
95
|
+
kind: z.literal("reframe"),
|
|
96
|
+
clip_id: z.string().uuid(),
|
|
97
|
+
}),
|
|
98
|
+
z.object({
|
|
99
|
+
kind: z.literal("speed"),
|
|
100
|
+
clip_id: z.string().uuid(),
|
|
101
|
+
numerator: z.number().int().positive(),
|
|
102
|
+
denominator: z.number().int().positive(),
|
|
103
|
+
reverse: z.boolean(),
|
|
104
|
+
audio_policy: z.enum(["preserve-pitch", "resample", "mute"]),
|
|
105
|
+
ripple: z.boolean().optional(),
|
|
106
|
+
timeline_limit_frame: z.number().int().positive().optional(),
|
|
107
|
+
}),
|
|
108
|
+
z.object({
|
|
109
|
+
kind: z.literal("transition"),
|
|
110
|
+
outgoing_clip_id: z.string().uuid(),
|
|
111
|
+
incoming_clip_id: z.string().uuid(),
|
|
112
|
+
transition_kind: z.enum(["cut", "dissolve"]).nullable(),
|
|
113
|
+
duration_frames: z.number().int().positive().optional(),
|
|
114
|
+
alignment: z.enum(["start", "center", "end"]).optional(),
|
|
115
|
+
}),
|
|
116
|
+
z.object({
|
|
117
|
+
kind: z.literal("caption"),
|
|
118
|
+
cue_id: z.string().uuid().optional(),
|
|
119
|
+
track_id: z.string().uuid().optional(),
|
|
120
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
121
|
+
duration_frames: z.number().int().positive(),
|
|
122
|
+
text: z.string().min(1),
|
|
123
|
+
style_id: z.enum(["clean", "bold", "high-contrast"]),
|
|
124
|
+
speaker: z.string().min(1).optional(),
|
|
125
|
+
}),
|
|
126
|
+
z.object({
|
|
127
|
+
kind: z.literal("music_video_setup"),
|
|
128
|
+
audio_artifact_id: z.string().uuid(),
|
|
129
|
+
audio_track_id: z.string().uuid(),
|
|
130
|
+
}),
|
|
131
|
+
z.object({
|
|
132
|
+
kind: z.literal("append_clips"),
|
|
133
|
+
track_id: z.string().uuid(),
|
|
134
|
+
artifact_ids: z.array(z.string().uuid()).min(1).max(20),
|
|
135
|
+
timeline_limit_frame: z.number().int().positive(),
|
|
136
|
+
fit: z.enum(["source", "fill"]).optional(),
|
|
137
|
+
visual_fit: z.enum(["fit", "fill"]).optional(),
|
|
138
|
+
}),
|
|
139
|
+
z.object({
|
|
140
|
+
kind: z.literal("reorder_clips"),
|
|
141
|
+
track_id: z.string().uuid(),
|
|
142
|
+
clip_ids: z.array(z.string().uuid()).min(1).max(500),
|
|
143
|
+
timeline_limit_frame: z.number().int().positive(),
|
|
144
|
+
}),
|
|
145
|
+
]);
|
|
146
|
+
const generationPlanSchema = z.object({
|
|
147
|
+
tool: z.string().regex(/^(?:generate|extend|upscale)_/u),
|
|
148
|
+
input: z.record(z.string(), z.unknown()),
|
|
149
|
+
provider: z.string().min(1),
|
|
150
|
+
model: z.string().min(1),
|
|
151
|
+
estimated_cost_usd: z.number().nonnegative().optional(),
|
|
152
|
+
estimate_unavailable: z.boolean().optional(),
|
|
153
|
+
track_id: z.string().uuid(),
|
|
154
|
+
timeline_start_frame: z.number().int().nonnegative(),
|
|
155
|
+
duration_frames: z.number().int().positive().optional(),
|
|
156
|
+
});
|
|
157
|
+
const UNSUPPORTED_CHAT_EDIT_MESSAGE = "I can safely plan insert, trim, volume, ducking, captions, move, split, delete, gaps, transforms, speed, and transitions. Name the action and its target.";
|
|
158
|
+
function json(value) {
|
|
159
|
+
return { content: [{ type: "text", text: JSON.stringify(value) }] };
|
|
160
|
+
}
|
|
161
|
+
function failure(message) {
|
|
162
|
+
return { isError: true, content: [{ type: "text", text: message }] };
|
|
163
|
+
}
|
|
164
|
+
function requiredTrack(sequence, trackId, kind) {
|
|
165
|
+
const track = trackId
|
|
166
|
+
? sequence.tracks.find((candidate) => candidate.trackId === trackId)
|
|
167
|
+
: sequence.tracks.find((candidate) => candidate.kind === kind && candidate.enabled && !candidate.locked);
|
|
168
|
+
if (!track || track.kind !== kind || !track.enabled || track.locked) {
|
|
169
|
+
throw new Error(`No editable ${kind} track is available`);
|
|
170
|
+
}
|
|
171
|
+
return track;
|
|
172
|
+
}
|
|
173
|
+
function clipEnd(clip) {
|
|
174
|
+
return clip.timelineStartFrame + clip.durationFrames;
|
|
175
|
+
}
|
|
176
|
+
function isTimedClip(clip) {
|
|
177
|
+
return clip.source.kind === "timed";
|
|
178
|
+
}
|
|
179
|
+
function clipsOnTrack(sequence, trackId) {
|
|
180
|
+
return sequence.clips
|
|
181
|
+
.filter((clip) => clip.trackId === trackId)
|
|
182
|
+
.sort((left, right) => left.timelineStartFrame - right.timelineStartFrame
|
|
183
|
+
|| left.clipId.localeCompare(right.clipId));
|
|
184
|
+
}
|
|
185
|
+
function timelineFramesForSource(ticks, timeBase, frameRate, speed) {
|
|
186
|
+
const frames = ticks
|
|
187
|
+
* timeBase.numerator
|
|
188
|
+
* frameRate.numerator
|
|
189
|
+
* speed.denominator
|
|
190
|
+
/ timeBase.denominator
|
|
191
|
+
/ frameRate.denominator
|
|
192
|
+
/ speed.numerator;
|
|
193
|
+
return Math.max(1, Math.ceil(frames));
|
|
194
|
+
}
|
|
195
|
+
function sourceTicksForTimeline(frames, sequence, clip) {
|
|
196
|
+
return sequenceFramesToSourceTicks(frames * clip.speed.numerator, sequence.frameRate, {
|
|
197
|
+
numerator: clip.source.range.timeBase.numerator * clip.speed.denominator,
|
|
198
|
+
denominator: clip.source.range.timeBase.denominator,
|
|
199
|
+
}, "nearest");
|
|
200
|
+
}
|
|
201
|
+
function shiftedTrackEnd(sequence, trackId, selectedClipId, selectedStart, selectedDuration, shiftFromFrame, deltaFrames) {
|
|
202
|
+
return Math.max(selectedStart + selectedDuration, ...clipsOnTrack(sequence, trackId)
|
|
203
|
+
.filter((clip) => clip.clipId !== selectedClipId)
|
|
204
|
+
.map((clip) => clipEnd(clip) + (clip.timelineStartFrame >= shiftFromFrame ? deltaFrames : 0)));
|
|
205
|
+
}
|
|
206
|
+
function requireTimelineLimit(limit, endFrame) {
|
|
207
|
+
if (limit !== undefined && endFrame > limit) {
|
|
208
|
+
throw new Error(`Music-video edit ends at frame ${endFrame}, beyond soundtrack frame ${limit}`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
function splitFrames(total, count) {
|
|
212
|
+
const base = Math.floor(total / count);
|
|
213
|
+
const extra = total % count;
|
|
214
|
+
return Array.from({ length: count }, (_, index) => base + (index >= count - extra ? 1 : 0));
|
|
215
|
+
}
|
|
216
|
+
function nativeClipFrames(sequence, clip) {
|
|
217
|
+
if (clip.source.kind !== "timed")
|
|
218
|
+
return clip.durationFrames;
|
|
219
|
+
return timelineFramesForSource(clip.source.range.durationTicks, clip.source.range.timeBase, sequence.frameRate, { numerator: 1, denominator: 1 });
|
|
220
|
+
}
|
|
221
|
+
function requiredVideoStream(engine, artifactId) {
|
|
222
|
+
const artifact = engine.artifacts.get(artifactId);
|
|
223
|
+
if (!artifact.ok)
|
|
224
|
+
throw new Error(artifact.error.message);
|
|
225
|
+
if (artifact.value.kind !== "video" && artifact.value.kind !== "final") {
|
|
226
|
+
throw new Error("Music-video sources must be video or final artifacts");
|
|
227
|
+
}
|
|
228
|
+
const streams = engine.streams.list(artifactId);
|
|
229
|
+
if (!streams.ok)
|
|
230
|
+
throw new Error(streams.error.message);
|
|
231
|
+
const stream = streams.value.find((candidate) => candidate.kind === "video");
|
|
232
|
+
if (!stream)
|
|
233
|
+
throw new Error(`Video source has no registered stream: ${artifactId}`);
|
|
234
|
+
return stream;
|
|
235
|
+
}
|
|
236
|
+
function timedVideoPlacement(trackId, timelineStartFrame, durationFrames, artifactId, stream, nativeFrames, visualFit) {
|
|
237
|
+
return {
|
|
238
|
+
kind: "insert-clip",
|
|
239
|
+
mode: "overwrite",
|
|
240
|
+
placement: {
|
|
241
|
+
trackId,
|
|
242
|
+
timelineStartFrame,
|
|
243
|
+
durationFrames,
|
|
244
|
+
source: {
|
|
245
|
+
kind: "timed",
|
|
246
|
+
artifactId,
|
|
247
|
+
range: {
|
|
248
|
+
streamId: stream.streamId,
|
|
249
|
+
objectHash: stream.objectHash,
|
|
250
|
+
startTick: 0,
|
|
251
|
+
durationTicks: stream.durationTicks,
|
|
252
|
+
timeBase: stream.timeBase,
|
|
253
|
+
},
|
|
254
|
+
},
|
|
255
|
+
speed: {
|
|
256
|
+
numerator: Math.max(1, nativeFrames),
|
|
257
|
+
denominator: Math.max(1, durationFrames),
|
|
258
|
+
},
|
|
259
|
+
reverse: false,
|
|
260
|
+
audioPolicy: "mute",
|
|
261
|
+
...(visualFit ? { transform: fittedTransform(visualFit) } : {}),
|
|
262
|
+
},
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
function rebalanceLaneAppend(sequence, trackId, existing, sources, limit) {
|
|
266
|
+
const count = existing.length + sources.length;
|
|
267
|
+
if (limit < count) {
|
|
268
|
+
throw new Error(`Music-video edit ends at frame ${count}, beyond soundtrack frame ${limit}`);
|
|
269
|
+
}
|
|
270
|
+
const slots = splitFrames(limit, count);
|
|
271
|
+
const items = [
|
|
272
|
+
...existing.map((clip, index) => ({
|
|
273
|
+
kind: "existing",
|
|
274
|
+
clipId: clip.clipId,
|
|
275
|
+
durationFrames: slots[index],
|
|
276
|
+
clip,
|
|
277
|
+
})),
|
|
278
|
+
...sources.map((source, index) => ({
|
|
279
|
+
kind: "source",
|
|
280
|
+
clipId: uuidv7(),
|
|
281
|
+
durationFrames: slots[existing.length + index],
|
|
282
|
+
source,
|
|
283
|
+
})),
|
|
284
|
+
];
|
|
285
|
+
const operations = [];
|
|
286
|
+
let temp = limit + 1;
|
|
287
|
+
for (const item of items) {
|
|
288
|
+
if (item.kind === "existing") {
|
|
289
|
+
const clip = item.clip;
|
|
290
|
+
operations.push({
|
|
291
|
+
kind: "move-clip",
|
|
292
|
+
clipId: item.clipId,
|
|
293
|
+
trackId,
|
|
294
|
+
timelineStartFrame: temp,
|
|
295
|
+
});
|
|
296
|
+
operations.push({
|
|
297
|
+
kind: "trim-clip",
|
|
298
|
+
clipId: item.clipId,
|
|
299
|
+
timelineStartFrame: temp,
|
|
300
|
+
durationFrames: item.durationFrames,
|
|
301
|
+
...(isTimedClip(clip)
|
|
302
|
+
? { sourceRange: clip.source.range }
|
|
303
|
+
: {}),
|
|
304
|
+
});
|
|
305
|
+
if (isTimedClip(clip)) {
|
|
306
|
+
operations.push({
|
|
307
|
+
kind: "set-clip-speed",
|
|
308
|
+
clipId: item.clipId,
|
|
309
|
+
speed: {
|
|
310
|
+
numerator: Math.max(1, nativeClipFrames(sequence, clip)),
|
|
311
|
+
denominator: Math.max(1, item.durationFrames),
|
|
312
|
+
},
|
|
313
|
+
reverse: clip.reverse,
|
|
314
|
+
audioPolicy: "mute",
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
temp += Math.max(clip.durationFrames, item.durationFrames) + 1;
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const source = item.source;
|
|
321
|
+
operations.push({
|
|
322
|
+
...timedVideoPlacement(trackId, temp, item.durationFrames, source.artifactId, source.stream, source.nativeFrames),
|
|
323
|
+
clipId: item.clipId,
|
|
324
|
+
});
|
|
325
|
+
temp += item.durationFrames + 1;
|
|
326
|
+
}
|
|
327
|
+
let start = 0;
|
|
328
|
+
for (const item of items) {
|
|
329
|
+
operations.push({
|
|
330
|
+
kind: "move-clip",
|
|
331
|
+
clipId: item.clipId,
|
|
332
|
+
trackId,
|
|
333
|
+
timelineStartFrame: start,
|
|
334
|
+
});
|
|
335
|
+
start += item.durationFrames;
|
|
336
|
+
}
|
|
337
|
+
requireTimelineLimit(limit, start);
|
|
338
|
+
return operations;
|
|
339
|
+
}
|
|
340
|
+
function rippleMoves(sequence, clip, deltaFrames) {
|
|
341
|
+
if (deltaFrames === 0)
|
|
342
|
+
return [];
|
|
343
|
+
const later = clipsOnTrack(sequence, clip.trackId)
|
|
344
|
+
.filter((candidate) => candidate.clipId !== clip.clipId
|
|
345
|
+
&& candidate.timelineStartFrame >= clipEnd(clip))
|
|
346
|
+
.sort((left, right) => deltaFrames > 0
|
|
347
|
+
? right.timelineStartFrame - left.timelineStartFrame
|
|
348
|
+
: left.timelineStartFrame - right.timelineStartFrame);
|
|
349
|
+
return later.map((candidate) => ({
|
|
350
|
+
kind: "move-clip",
|
|
351
|
+
clipId: candidate.clipId,
|
|
352
|
+
trackId: candidate.trackId,
|
|
353
|
+
timelineStartFrame: candidate.timelineStartFrame + deltaFrames,
|
|
354
|
+
}));
|
|
355
|
+
}
|
|
356
|
+
function clipAudio(clip, muted) {
|
|
357
|
+
return {
|
|
358
|
+
gainDb: clip.audio?.gainDb ?? 0,
|
|
359
|
+
muted,
|
|
360
|
+
fadeInFrames: clip.audio?.fadeInFrames ?? 0,
|
|
361
|
+
fadeOutFrames: clip.audio?.fadeOutFrames ?? 0,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
function audioSetupOperations(engine, sequence, audioArtifactId, audioTrackId) {
|
|
365
|
+
const artifact = engine.artifacts.get(audioArtifactId);
|
|
366
|
+
if (!artifact.ok)
|
|
367
|
+
throw new Error(artifact.error.message);
|
|
368
|
+
if (artifact.value.kind !== "audio") {
|
|
369
|
+
throw new Error("Music-video setup requires an audio artifact");
|
|
370
|
+
}
|
|
371
|
+
const track = requiredTrack(sequence, audioTrackId, "audio");
|
|
372
|
+
if (track.muted)
|
|
373
|
+
throw new Error("The soundtrack track is muted");
|
|
374
|
+
if (sequence.tracks.some((candidate) => candidate.kind === "audio"
|
|
375
|
+
&& candidate.trackId !== track.trackId
|
|
376
|
+
&& candidate.solo)) {
|
|
377
|
+
throw new Error("Disable solo on other audio tracks before applying the soundtrack");
|
|
378
|
+
}
|
|
379
|
+
const streams = engine.streams.list(audioArtifactId);
|
|
380
|
+
if (!streams.ok)
|
|
381
|
+
throw new Error(streams.error.message);
|
|
382
|
+
const stream = streams.value.find((candidate) => candidate.kind === "audio");
|
|
383
|
+
if (!stream)
|
|
384
|
+
throw new Error("The soundtrack has no registered audio stream");
|
|
385
|
+
const durationFrames = sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil");
|
|
386
|
+
const destinationClips = clipsOnTrack(sequence, track.trackId);
|
|
387
|
+
const existingMaster = destinationClips.length === 1
|
|
388
|
+
&& destinationClips[0]?.source.kind === "timed"
|
|
389
|
+
&& destinationClips[0].source.artifactId === audioArtifactId
|
|
390
|
+
&& destinationClips[0].timelineStartFrame === 0
|
|
391
|
+
? destinationClips[0]
|
|
392
|
+
: null;
|
|
393
|
+
if (destinationClips.length > 0 && !existingMaster) {
|
|
394
|
+
throw new Error("Choose an empty audio track for the master soundtrack");
|
|
395
|
+
}
|
|
396
|
+
const operations = existingMaster
|
|
397
|
+
? [
|
|
398
|
+
{
|
|
399
|
+
kind: "trim-clip",
|
|
400
|
+
clipId: existingMaster.clipId,
|
|
401
|
+
timelineStartFrame: 0,
|
|
402
|
+
durationFrames,
|
|
403
|
+
sourceRange: {
|
|
404
|
+
streamId: stream.streamId,
|
|
405
|
+
objectHash: stream.objectHash,
|
|
406
|
+
startTick: 0,
|
|
407
|
+
durationTicks: stream.durationTicks,
|
|
408
|
+
timeBase: stream.timeBase,
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
{
|
|
412
|
+
kind: "set-clip-speed",
|
|
413
|
+
clipId: existingMaster.clipId,
|
|
414
|
+
speed: { numerator: 1, denominator: 1 },
|
|
415
|
+
reverse: false,
|
|
416
|
+
audioPolicy: "preserve-pitch",
|
|
417
|
+
},
|
|
418
|
+
{
|
|
419
|
+
kind: "set-clip-audio",
|
|
420
|
+
clipId: existingMaster.clipId,
|
|
421
|
+
audio: clipAudio(existingMaster, false),
|
|
422
|
+
},
|
|
423
|
+
]
|
|
424
|
+
: [{
|
|
425
|
+
kind: "insert-clip",
|
|
426
|
+
mode: "insert",
|
|
427
|
+
placement: {
|
|
428
|
+
trackId: track.trackId,
|
|
429
|
+
timelineStartFrame: 0,
|
|
430
|
+
durationFrames,
|
|
431
|
+
source: {
|
|
432
|
+
kind: "timed",
|
|
433
|
+
artifactId: audioArtifactId,
|
|
434
|
+
range: {
|
|
435
|
+
streamId: stream.streamId,
|
|
436
|
+
objectHash: stream.objectHash,
|
|
437
|
+
startTick: 0,
|
|
438
|
+
durationTicks: stream.durationTicks,
|
|
439
|
+
timeBase: stream.timeBase,
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
speed: { numerator: 1, denominator: 1 },
|
|
443
|
+
reverse: false,
|
|
444
|
+
audioPolicy: "preserve-pitch",
|
|
445
|
+
audio: {
|
|
446
|
+
gainDb: 0,
|
|
447
|
+
muted: false,
|
|
448
|
+
fadeInFrames: 0,
|
|
449
|
+
fadeOutFrames: 0,
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
}];
|
|
453
|
+
for (const clip of sequence.clips) {
|
|
454
|
+
if (clip.clipId === existingMaster?.clipId || !clip.enabled)
|
|
455
|
+
continue;
|
|
456
|
+
const clipTrack = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
|
|
457
|
+
if (!clipTrack?.enabled)
|
|
458
|
+
continue;
|
|
459
|
+
if (clipTrack.kind === "audio") {
|
|
460
|
+
if (clipTrack.muted || clip.audio?.muted)
|
|
461
|
+
continue;
|
|
462
|
+
if (clipTrack.locked) {
|
|
463
|
+
throw new Error(`Unlock audio track ${clipTrack.name} before applying the soundtrack`);
|
|
464
|
+
}
|
|
465
|
+
operations.push({
|
|
466
|
+
kind: "set-clip-audio",
|
|
467
|
+
clipId: clip.clipId,
|
|
468
|
+
audio: clipAudio(clip, true),
|
|
469
|
+
});
|
|
470
|
+
continue;
|
|
471
|
+
}
|
|
472
|
+
if (clipTrack.kind === "video"
|
|
473
|
+
&& isTimedClip(clip)
|
|
474
|
+
&& clip.audioPolicy !== "mute") {
|
|
475
|
+
if (clipTrack.locked) {
|
|
476
|
+
throw new Error(`Unlock video track ${clipTrack.name} before muting embedded audio`);
|
|
477
|
+
}
|
|
478
|
+
operations.push({
|
|
479
|
+
kind: "set-clip-speed",
|
|
480
|
+
clipId: clip.clipId,
|
|
481
|
+
speed: clip.speed,
|
|
482
|
+
reverse: clip.reverse,
|
|
483
|
+
audioPolicy: "mute",
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return operations;
|
|
488
|
+
}
|
|
489
|
+
function rippleTrimOperations(engine, sequence, request, clip) {
|
|
490
|
+
if (!request.edge)
|
|
491
|
+
throw new Error("Ripple trim requires a start or end edge");
|
|
492
|
+
if (isTimedClip(clip) && clip.reverse) {
|
|
493
|
+
throw new Error("Ripple trim is unavailable for reversed clips");
|
|
494
|
+
}
|
|
495
|
+
const oldEnd = clipEnd(clip);
|
|
496
|
+
const newEnd = request.timeline_start_frame + request.duration_frames;
|
|
497
|
+
if (request.edge === "start" && newEnd !== oldEnd) {
|
|
498
|
+
throw new Error("A start-edge trim must preserve the clip end frame");
|
|
499
|
+
}
|
|
500
|
+
if (request.edge === "end"
|
|
501
|
+
&& request.timeline_start_frame !== clip.timelineStartFrame) {
|
|
502
|
+
throw new Error("An end-edge trim must preserve the clip start frame");
|
|
503
|
+
}
|
|
504
|
+
let sourceRange = isTimedClip(clip)
|
|
505
|
+
? { ...clip.source.range }
|
|
506
|
+
: undefined;
|
|
507
|
+
if (isTimedClip(clip)) {
|
|
508
|
+
const stream = engine.streams.get(clip.source.range.streamId);
|
|
509
|
+
if (!stream.ok)
|
|
510
|
+
throw new Error(stream.error.message);
|
|
511
|
+
const durationTicks = sourceTicksForTimeline(request.duration_frames, sequence, clip);
|
|
512
|
+
const sourceEndTick = clip.source.range.startTick
|
|
513
|
+
+ clip.source.range.durationTicks;
|
|
514
|
+
const startTick = request.edge === "start"
|
|
515
|
+
? sourceEndTick - durationTicks
|
|
516
|
+
: clip.source.range.startTick;
|
|
517
|
+
if (startTick < 0
|
|
518
|
+
|| durationTicks < 1
|
|
519
|
+
|| startTick + durationTicks > stream.value.durationTicks) {
|
|
520
|
+
throw new Error("Trim exceeds the registered source handles");
|
|
521
|
+
}
|
|
522
|
+
sourceRange = {
|
|
523
|
+
...clip.source.range,
|
|
524
|
+
startTick,
|
|
525
|
+
durationTicks,
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
const deltaFrames = newEnd - oldEnd;
|
|
529
|
+
requireTimelineLimit(request.timeline_limit_frame, shiftedTrackEnd(sequence, clip.trackId, clip.clipId, request.timeline_start_frame, request.duration_frames, oldEnd, deltaFrames));
|
|
530
|
+
const trim = {
|
|
531
|
+
kind: "trim-clip",
|
|
532
|
+
clipId: clip.clipId,
|
|
533
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
534
|
+
durationFrames: request.duration_frames,
|
|
535
|
+
...(sourceRange ? { sourceRange } : {}),
|
|
536
|
+
};
|
|
537
|
+
const moves = rippleMoves(sequence, clip, deltaFrames);
|
|
538
|
+
return deltaFrames > 0 ? [...moves, trim] : [trim, ...moves];
|
|
539
|
+
}
|
|
540
|
+
function rippleSpeedOperations(sequence, request, clip) {
|
|
541
|
+
const durationFrames = timelineFramesForSource(clip.source.range.durationTicks, clip.source.range.timeBase, sequence.frameRate, { numerator: request.numerator, denominator: request.denominator });
|
|
542
|
+
const deltaFrames = durationFrames - clip.durationFrames;
|
|
543
|
+
requireTimelineLimit(request.timeline_limit_frame, shiftedTrackEnd(sequence, clip.trackId, clip.clipId, clip.timelineStartFrame, durationFrames, clipEnd(clip), deltaFrames));
|
|
544
|
+
const trim = {
|
|
545
|
+
kind: "trim-clip",
|
|
546
|
+
clipId: clip.clipId,
|
|
547
|
+
timelineStartFrame: clip.timelineStartFrame,
|
|
548
|
+
durationFrames,
|
|
549
|
+
sourceRange: clip.source.range,
|
|
550
|
+
};
|
|
551
|
+
const speed = {
|
|
552
|
+
kind: "set-clip-speed",
|
|
553
|
+
clipId: clip.clipId,
|
|
554
|
+
speed: {
|
|
555
|
+
numerator: request.numerator,
|
|
556
|
+
denominator: request.denominator,
|
|
557
|
+
},
|
|
558
|
+
reverse: request.reverse,
|
|
559
|
+
audioPolicy: request.audio_policy,
|
|
560
|
+
};
|
|
561
|
+
const moves = rippleMoves(sequence, clip, deltaFrames);
|
|
562
|
+
return deltaFrames > 0
|
|
563
|
+
? [...moves, trim, speed]
|
|
564
|
+
: [trim, speed, ...moves];
|
|
565
|
+
}
|
|
566
|
+
async function operationsForRequest(engine, sequence, request) {
|
|
567
|
+
if (request.kind === "remove_clip") {
|
|
568
|
+
return [{ kind: "remove-clip", clipId: request.clip_id, ripple: request.ripple }];
|
|
569
|
+
}
|
|
570
|
+
if (request.kind === "append") {
|
|
571
|
+
const track = sequence.tracks.find((entry) => entry.trackId === request.track_id);
|
|
572
|
+
if (!track || track.kind === "caption")
|
|
573
|
+
throw new Error("Choose a video or audio track");
|
|
574
|
+
if (track.locked)
|
|
575
|
+
throw new Error("The destination track is locked");
|
|
576
|
+
const artifact = engine.artifacts.get(request.artifact_id);
|
|
577
|
+
if (!artifact.ok)
|
|
578
|
+
throw new Error(artifact.error.message);
|
|
579
|
+
const compatible = track.kind === "audio"
|
|
580
|
+
? artifact.value.kind === "audio"
|
|
581
|
+
: artifact.value.kind === "video" || artifact.value.kind === "final";
|
|
582
|
+
if (!compatible)
|
|
583
|
+
throw new Error(`Choose ${track.kind === "audio" ? "an audio" : "a video"} source for this track`);
|
|
584
|
+
const manifest = await engine.files.manifest(request.artifact_id);
|
|
585
|
+
if (!manifest.ok)
|
|
586
|
+
throw new Error(manifest.error.message);
|
|
587
|
+
const file = track.kind === "audio"
|
|
588
|
+
? findAudioFile(manifest.value.files, artifact.value.kind)
|
|
589
|
+
: findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
|
|
590
|
+
const streams = engine.streams.list(request.artifact_id);
|
|
591
|
+
if (!streams.ok)
|
|
592
|
+
throw new Error(streams.error.message);
|
|
593
|
+
const stream = streams.value.find((entry) => entry.kind === track.kind
|
|
594
|
+
&& entry.sourcePath === file?.name && entry.objectHash === file.objectHash);
|
|
595
|
+
if (!stream)
|
|
596
|
+
throw new Error("Wait for the current source media to finish processing before adding it");
|
|
597
|
+
const durationFrames = sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil");
|
|
598
|
+
return [{
|
|
599
|
+
kind: "insert-clip",
|
|
600
|
+
mode: "insert",
|
|
601
|
+
placement: {
|
|
602
|
+
trackId: track.trackId,
|
|
603
|
+
timelineStartFrame: sequence.clips.reduce((end, clip) => clip.trackId === track.trackId ? Math.max(end, clipEnd(clip)) : end, 0),
|
|
604
|
+
durationFrames,
|
|
605
|
+
source: {
|
|
606
|
+
kind: "timed",
|
|
607
|
+
artifactId: request.artifact_id,
|
|
608
|
+
range: {
|
|
609
|
+
streamId: stream.streamId,
|
|
610
|
+
objectHash: stream.objectHash,
|
|
611
|
+
startTick: 0,
|
|
612
|
+
durationTicks: stream.durationTicks,
|
|
613
|
+
timeBase: stream.timeBase,
|
|
614
|
+
},
|
|
615
|
+
},
|
|
616
|
+
speed: { numerator: 1, denominator: 1 },
|
|
617
|
+
reverse: false,
|
|
618
|
+
audioPolicy: "preserve-pitch",
|
|
619
|
+
},
|
|
620
|
+
}];
|
|
621
|
+
}
|
|
622
|
+
if (request.kind === "music_video_setup") {
|
|
623
|
+
return audioSetupOperations(engine, sequence, request.audio_artifact_id, request.audio_track_id);
|
|
624
|
+
}
|
|
625
|
+
if (request.kind === "append_clips") {
|
|
626
|
+
const track = requiredTrack(sequence, request.track_id, "video");
|
|
627
|
+
const existing = clipsOnTrack(sequence, track.trackId);
|
|
628
|
+
const sources = request.artifact_ids.map((artifactId) => {
|
|
629
|
+
const stream = requiredVideoStream(engine, artifactId);
|
|
630
|
+
return {
|
|
631
|
+
artifactId,
|
|
632
|
+
stream,
|
|
633
|
+
nativeFrames: sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "ceil"),
|
|
634
|
+
};
|
|
635
|
+
});
|
|
636
|
+
if ((request.fit ?? "fill") === "source") {
|
|
637
|
+
let timelineStartFrame = Math.max(0, ...existing.map(clipEnd));
|
|
638
|
+
const operations = [];
|
|
639
|
+
for (const source of sources) {
|
|
640
|
+
requireTimelineLimit(request.timeline_limit_frame, timelineStartFrame + source.nativeFrames);
|
|
641
|
+
operations.push(timedVideoPlacement(track.trackId, timelineStartFrame, source.nativeFrames, source.artifactId, source.stream, source.nativeFrames, request.visual_fit));
|
|
642
|
+
timelineStartFrame += source.nativeFrames;
|
|
643
|
+
}
|
|
644
|
+
return operations;
|
|
645
|
+
}
|
|
646
|
+
const laneEnd = Math.max(0, ...existing.map(clipEnd));
|
|
647
|
+
if (laneEnd > request.timeline_limit_frame) {
|
|
648
|
+
requireTimelineLimit(request.timeline_limit_frame, laneEnd);
|
|
649
|
+
}
|
|
650
|
+
return rebalanceLaneAppend(sequence, track.trackId, existing, sources, request.timeline_limit_frame);
|
|
651
|
+
}
|
|
652
|
+
if (request.kind === "reorder_clips") {
|
|
653
|
+
const track = requiredTrack(sequence, request.track_id, "video");
|
|
654
|
+
const current = clipsOnTrack(sequence, track.trackId);
|
|
655
|
+
const currentIds = new Set(current.map((clip) => clip.clipId));
|
|
656
|
+
const requestedIds = new Set(request.clip_ids);
|
|
657
|
+
if (current.length !== request.clip_ids.length
|
|
658
|
+
|| currentIds.size !== requestedIds.size
|
|
659
|
+
|| request.clip_ids.some((clipId) => !currentIds.has(clipId))) {
|
|
660
|
+
throw new Error("Reorder requires every clip on the main video lane exactly once");
|
|
661
|
+
}
|
|
662
|
+
const byId = new Map(current.map((clip) => [clip.clipId, clip]));
|
|
663
|
+
const gaps = current.map((clip, index) => index === 0
|
|
664
|
+
? clip.timelineStartFrame
|
|
665
|
+
: clip.timelineStartFrame - clipEnd(current[index - 1]));
|
|
666
|
+
const finalStarts = new Map();
|
|
667
|
+
let cursor = gaps[0] ?? 0;
|
|
668
|
+
request.clip_ids.forEach((clipId, index) => {
|
|
669
|
+
if (index > 0)
|
|
670
|
+
cursor += gaps[index] ?? 0;
|
|
671
|
+
finalStarts.set(clipId, cursor);
|
|
672
|
+
cursor += byId.get(clipId).durationFrames;
|
|
673
|
+
});
|
|
674
|
+
requireTimelineLimit(request.timeline_limit_frame, cursor);
|
|
675
|
+
const unchanged = request.clip_ids.every((clipId, index) => clipId === current[index]?.clipId);
|
|
676
|
+
if (unchanged) {
|
|
677
|
+
return current.map((clip) => ({
|
|
678
|
+
kind: "move-clip",
|
|
679
|
+
clipId: clip.clipId,
|
|
680
|
+
trackId: track.trackId,
|
|
681
|
+
timelineStartFrame: clip.timelineStartFrame,
|
|
682
|
+
}));
|
|
683
|
+
}
|
|
684
|
+
let temporaryCursor = Math.max(request.timeline_limit_frame, ...sequence.clips.map(clipEnd)) + 1;
|
|
685
|
+
const temporary = current.map((clip) => {
|
|
686
|
+
const operation = {
|
|
687
|
+
kind: "move-clip",
|
|
688
|
+
clipId: clip.clipId,
|
|
689
|
+
trackId: track.trackId,
|
|
690
|
+
timelineStartFrame: temporaryCursor,
|
|
691
|
+
};
|
|
692
|
+
temporaryCursor += clip.durationFrames + 1;
|
|
693
|
+
return operation;
|
|
694
|
+
});
|
|
695
|
+
const final = request.clip_ids.map((clipId) => ({
|
|
696
|
+
kind: "move-clip",
|
|
697
|
+
clipId,
|
|
698
|
+
trackId: track.trackId,
|
|
699
|
+
timelineStartFrame: finalStarts.get(clipId),
|
|
700
|
+
}));
|
|
701
|
+
return [...temporary, ...final];
|
|
702
|
+
}
|
|
703
|
+
if (request.kind === "trim") {
|
|
704
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
705
|
+
if (!clip)
|
|
706
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
707
|
+
if (request.ripple) {
|
|
708
|
+
return rippleTrimOperations(engine, sequence, request, clip);
|
|
709
|
+
}
|
|
710
|
+
return [{
|
|
711
|
+
kind: "trim-clip",
|
|
712
|
+
clipId: clip.clipId,
|
|
713
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
714
|
+
durationFrames: request.duration_frames,
|
|
715
|
+
}];
|
|
716
|
+
}
|
|
717
|
+
if (request.kind === "volume") {
|
|
718
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
719
|
+
if (!clip)
|
|
720
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
721
|
+
const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
|
|
722
|
+
if (track?.kind !== "audio") {
|
|
723
|
+
throw new Error("Volume changes require a clip on an audio track");
|
|
724
|
+
}
|
|
725
|
+
const current = clip.audio ?? {
|
|
726
|
+
gainDb: 0,
|
|
727
|
+
muted: false,
|
|
728
|
+
fadeInFrames: 0,
|
|
729
|
+
fadeOutFrames: 0,
|
|
730
|
+
};
|
|
731
|
+
return [{
|
|
732
|
+
kind: "set-clip-audio",
|
|
733
|
+
clipId: clip.clipId,
|
|
734
|
+
audio: {
|
|
735
|
+
gainDb: request.gain_db,
|
|
736
|
+
muted: request.muted ?? current.muted,
|
|
737
|
+
fadeInFrames: request.fade_in_frames ?? current.fadeInFrames,
|
|
738
|
+
fadeOutFrames: request.fade_out_frames ?? current.fadeOutFrames,
|
|
739
|
+
},
|
|
740
|
+
}];
|
|
741
|
+
}
|
|
742
|
+
if (request.kind === "duck") {
|
|
743
|
+
const narration = sequence.clips.find((candidate) => candidate.clipId === request.narration_clip_id);
|
|
744
|
+
const narrationTrack = narration && sequence.tracks.find((candidate) => candidate.trackId === narration.trackId);
|
|
745
|
+
if (!narration || narrationTrack?.kind !== "audio") {
|
|
746
|
+
throw new Error("Ducking requires a narration clip on an audio track");
|
|
747
|
+
}
|
|
748
|
+
const narrationEnd = narration.timelineStartFrame + narration.durationFrames;
|
|
749
|
+
const overlaps = sequence.clips.filter((clip) => {
|
|
750
|
+
const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
|
|
751
|
+
const clipEnd = clip.timelineStartFrame + clip.durationFrames;
|
|
752
|
+
return clip.clipId !== narration.clipId
|
|
753
|
+
&& track?.kind === "audio"
|
|
754
|
+
&& track.enabled
|
|
755
|
+
&& !track.locked
|
|
756
|
+
&& clip.enabled
|
|
757
|
+
&& clip.timelineStartFrame < narrationEnd
|
|
758
|
+
&& clipEnd > narration.timelineStartFrame;
|
|
759
|
+
});
|
|
760
|
+
if (overlaps.length === 0) {
|
|
761
|
+
throw new Error("No editable audio clips overlap the narration");
|
|
762
|
+
}
|
|
763
|
+
return overlaps.map((clip) => {
|
|
764
|
+
const current = clip.audio ?? {
|
|
765
|
+
gainDb: 0,
|
|
766
|
+
muted: false,
|
|
767
|
+
fadeInFrames: 0,
|
|
768
|
+
fadeOutFrames: 0,
|
|
769
|
+
};
|
|
770
|
+
return {
|
|
771
|
+
kind: "set-clip-audio",
|
|
772
|
+
clipId: clip.clipId,
|
|
773
|
+
audio: {
|
|
774
|
+
...current,
|
|
775
|
+
gainDb: Math.max(-96, current.gainDb + request.reduction_db),
|
|
776
|
+
},
|
|
777
|
+
};
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
if (request.kind === "move") {
|
|
781
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
782
|
+
if (!clip)
|
|
783
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
784
|
+
const target = sequence.tracks.find((candidate) => candidate.trackId === request.track_id);
|
|
785
|
+
const source = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
|
|
786
|
+
if (!target
|
|
787
|
+
|| !source
|
|
788
|
+
|| target.kind !== source.kind
|
|
789
|
+
|| target.locked
|
|
790
|
+
|| !target.enabled) {
|
|
791
|
+
throw new Error("Move destination is not a compatible editable track");
|
|
792
|
+
}
|
|
793
|
+
return [{
|
|
794
|
+
kind: "move-clip",
|
|
795
|
+
clipId: clip.clipId,
|
|
796
|
+
trackId: target.trackId,
|
|
797
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
798
|
+
}];
|
|
799
|
+
}
|
|
800
|
+
if (request.kind === "split") {
|
|
801
|
+
if (!sequence.clips.some((candidate) => candidate.clipId === request.clip_id)) {
|
|
802
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
803
|
+
}
|
|
804
|
+
return [{
|
|
805
|
+
kind: "split-clip",
|
|
806
|
+
clipId: request.clip_id,
|
|
807
|
+
splitFrame: request.split_frame,
|
|
808
|
+
}];
|
|
809
|
+
}
|
|
810
|
+
if (request.kind === "delete") {
|
|
811
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
812
|
+
if (!clip)
|
|
813
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
814
|
+
return [{
|
|
815
|
+
kind: "remove-range",
|
|
816
|
+
trackIds: [clip.trackId],
|
|
817
|
+
range: {
|
|
818
|
+
sequenceId: sequence.sequenceId,
|
|
819
|
+
startFrame: clip.timelineStartFrame,
|
|
820
|
+
durationFrames: clip.durationFrames,
|
|
821
|
+
},
|
|
822
|
+
ripple: request.ripple,
|
|
823
|
+
}];
|
|
824
|
+
}
|
|
825
|
+
if (request.kind === "gap") {
|
|
826
|
+
const trackIds = request.track_ids ?? sequence.tracks
|
|
827
|
+
.filter((track) => track.kind !== "caption" && track.enabled && !track.locked)
|
|
828
|
+
.map((track) => track.trackId);
|
|
829
|
+
const selected = new Set(trackIds);
|
|
830
|
+
const clips = sequence.clips
|
|
831
|
+
.filter((clip) => selected.has(clip.trackId)
|
|
832
|
+
&& clip.timelineStartFrame >= request.timeline_start_frame)
|
|
833
|
+
.sort((left, right) => right.timelineStartFrame - left.timelineStartFrame);
|
|
834
|
+
if (clips.length === 0)
|
|
835
|
+
throw new Error("No clips follow the requested gap");
|
|
836
|
+
return clips.map((clip) => ({
|
|
837
|
+
kind: "move-clip",
|
|
838
|
+
clipId: clip.clipId,
|
|
839
|
+
trackId: clip.trackId,
|
|
840
|
+
timelineStartFrame: clip.timelineStartFrame + request.duration_frames,
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
if (request.kind === "transform") {
|
|
844
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
845
|
+
if (!clip)
|
|
846
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
847
|
+
const current = clip.transform ?? {
|
|
848
|
+
fit: "fill",
|
|
849
|
+
positionX: 0,
|
|
850
|
+
positionY: 0,
|
|
851
|
+
scaleX: 1,
|
|
852
|
+
scaleY: 1,
|
|
853
|
+
anchorX: 0.5,
|
|
854
|
+
anchorY: 0.5,
|
|
855
|
+
rotationDegrees: 0,
|
|
856
|
+
cropTop: 0,
|
|
857
|
+
cropRight: 0,
|
|
858
|
+
cropBottom: 0,
|
|
859
|
+
cropLeft: 0,
|
|
860
|
+
opacity: 1,
|
|
861
|
+
blendMode: "normal",
|
|
862
|
+
};
|
|
863
|
+
return [{
|
|
864
|
+
kind: "set-clip-transform",
|
|
865
|
+
clipId: clip.clipId,
|
|
866
|
+
transform: {
|
|
867
|
+
...current,
|
|
868
|
+
fit: request.fit ?? current.fit,
|
|
869
|
+
positionX: request.position_x ?? current.positionX,
|
|
870
|
+
positionY: request.position_y ?? current.positionY,
|
|
871
|
+
scaleX: request.scale_x ?? current.scaleX,
|
|
872
|
+
scaleY: request.scale_y ?? current.scaleY,
|
|
873
|
+
rotationDegrees: request.rotation_degrees ?? current.rotationDegrees,
|
|
874
|
+
opacity: request.opacity ?? current.opacity,
|
|
875
|
+
cropTop: request.crop_top ?? current.cropTop,
|
|
876
|
+
cropRight: request.crop_right ?? current.cropRight,
|
|
877
|
+
cropBottom: request.crop_bottom ?? current.cropBottom,
|
|
878
|
+
cropLeft: request.crop_left ?? current.cropLeft,
|
|
879
|
+
},
|
|
880
|
+
}];
|
|
881
|
+
}
|
|
882
|
+
if (request.kind === "reframe") {
|
|
883
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
884
|
+
if (!clip)
|
|
885
|
+
throw new Error(`Sequence clip not found: ${request.clip_id}`);
|
|
886
|
+
const track = sequence.tracks.find((candidate) => candidate.trackId === clip.trackId);
|
|
887
|
+
if (track?.kind !== "video") {
|
|
888
|
+
throw new Error("Automatic reframe requires a visual clip");
|
|
889
|
+
}
|
|
890
|
+
const analysis = await engine.files.read(clip.source.artifactId, ".original.analysis.json");
|
|
891
|
+
let subjectPlacement = "center";
|
|
892
|
+
if (analysis.ok) {
|
|
893
|
+
try {
|
|
894
|
+
const parsed = JSON.parse(typeof analysis.value === "string"
|
|
895
|
+
? analysis.value
|
|
896
|
+
: new TextDecoder().decode(analysis.value));
|
|
897
|
+
if (parsed.subjectPlacement === "left"
|
|
898
|
+
|| parsed.subjectPlacement === "right") {
|
|
899
|
+
subjectPlacement = parsed.subjectPlacement;
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
catch {
|
|
903
|
+
subjectPlacement = "center";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
const current = clip.transform ?? {
|
|
907
|
+
fit: "fill",
|
|
908
|
+
positionX: 0,
|
|
909
|
+
positionY: 0,
|
|
910
|
+
scaleX: 1,
|
|
911
|
+
scaleY: 1,
|
|
912
|
+
anchorX: 0.5,
|
|
913
|
+
anchorY: 0.5,
|
|
914
|
+
rotationDegrees: 0,
|
|
915
|
+
cropTop: 0,
|
|
916
|
+
cropRight: 0,
|
|
917
|
+
cropBottom: 0,
|
|
918
|
+
cropLeft: 0,
|
|
919
|
+
opacity: 1,
|
|
920
|
+
blendMode: "normal",
|
|
921
|
+
};
|
|
922
|
+
return [{
|
|
923
|
+
kind: "set-clip-transform",
|
|
924
|
+
clipId: clip.clipId,
|
|
925
|
+
transform: {
|
|
926
|
+
...current,
|
|
927
|
+
fit: "fill",
|
|
928
|
+
positionX: subjectPlacement === "left"
|
|
929
|
+
? sequence.width * 0.18
|
|
930
|
+
: subjectPlacement === "right"
|
|
931
|
+
? sequence.width * -0.18
|
|
932
|
+
: 0,
|
|
933
|
+
},
|
|
934
|
+
}];
|
|
935
|
+
}
|
|
936
|
+
if (request.kind === "speed") {
|
|
937
|
+
const clip = sequence.clips.find((candidate) => candidate.clipId === request.clip_id);
|
|
938
|
+
if (!clip || !isTimedClip(clip)) {
|
|
939
|
+
throw new Error("Speed and reverse require a timed sequence clip");
|
|
940
|
+
}
|
|
941
|
+
if (request.ripple) {
|
|
942
|
+
return rippleSpeedOperations(sequence, request, clip);
|
|
943
|
+
}
|
|
944
|
+
return [{
|
|
945
|
+
kind: "set-clip-speed",
|
|
946
|
+
clipId: clip.clipId,
|
|
947
|
+
speed: {
|
|
948
|
+
numerator: request.numerator,
|
|
949
|
+
denominator: request.denominator,
|
|
950
|
+
},
|
|
951
|
+
reverse: request.reverse,
|
|
952
|
+
audioPolicy: request.audio_policy,
|
|
953
|
+
}];
|
|
954
|
+
}
|
|
955
|
+
if (request.kind === "transition") {
|
|
956
|
+
const outgoing = sequence.clips.find((candidate) => candidate.clipId === request.outgoing_clip_id);
|
|
957
|
+
const incoming = sequence.clips.find((candidate) => candidate.clipId === request.incoming_clip_id);
|
|
958
|
+
if (!outgoing || !incoming || outgoing.trackId !== incoming.trackId) {
|
|
959
|
+
throw new Error("Transition clips must exist on the same track");
|
|
960
|
+
}
|
|
961
|
+
return [{
|
|
962
|
+
kind: "set-transition",
|
|
963
|
+
outgoingClipId: outgoing.clipId,
|
|
964
|
+
incomingClipId: incoming.clipId,
|
|
965
|
+
transition: request.transition_kind === null
|
|
966
|
+
? null
|
|
967
|
+
: {
|
|
968
|
+
transitionId: uuidv7(),
|
|
969
|
+
trackId: outgoing.trackId,
|
|
970
|
+
outgoingClipId: outgoing.clipId,
|
|
971
|
+
incomingClipId: incoming.clipId,
|
|
972
|
+
kind: request.transition_kind,
|
|
973
|
+
durationFrames: request.duration_frames ?? 12,
|
|
974
|
+
alignment: request.alignment ?? "center",
|
|
975
|
+
},
|
|
976
|
+
}];
|
|
977
|
+
}
|
|
978
|
+
if (request.kind === "caption") {
|
|
979
|
+
const track = request.track_id
|
|
980
|
+
? sequence.tracks.find((candidate) => candidate.trackId === request.track_id)
|
|
981
|
+
: sequence.tracks.find((candidate) => candidate.kind === "caption" && candidate.enabled && !candidate.locked);
|
|
982
|
+
if (!track || track.kind !== "caption" || track.locked || !track.enabled) {
|
|
983
|
+
throw new Error("No editable caption track is available");
|
|
984
|
+
}
|
|
985
|
+
return [{
|
|
986
|
+
kind: "upsert-caption-cue",
|
|
987
|
+
cue: {
|
|
988
|
+
cueId: request.cue_id ?? uuidv7(),
|
|
989
|
+
trackId: track.trackId,
|
|
990
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
991
|
+
durationFrames: request.duration_frames,
|
|
992
|
+
text: request.text,
|
|
993
|
+
styleId: request.style_id,
|
|
994
|
+
...(request.speaker ? { speaker: request.speaker } : {}),
|
|
995
|
+
},
|
|
996
|
+
}];
|
|
997
|
+
}
|
|
998
|
+
const artifact = engine.artifacts.get(request.artifact_id);
|
|
999
|
+
if (!artifact.ok)
|
|
1000
|
+
throw new Error(artifact.error.message);
|
|
1001
|
+
const streams = engine.streams.list(artifact.value.artifactId);
|
|
1002
|
+
if (!streams.ok)
|
|
1003
|
+
throw new Error(streams.error.message);
|
|
1004
|
+
const timedStream = streams.value.find((stream) => stream.kind === "video") ?? streams.value.find((stream) => stream.kind === "audio");
|
|
1005
|
+
if (timedStream) {
|
|
1006
|
+
const track = requiredTrack(sequence, request.track_id, timedStream.kind);
|
|
1007
|
+
const sourceStartTick = request.source_start_tick ?? 0;
|
|
1008
|
+
const sourceDurationTicks = request.source_duration_ticks
|
|
1009
|
+
?? timedStream.durationTicks - sourceStartTick;
|
|
1010
|
+
if (sourceDurationTicks < 1
|
|
1011
|
+
|| sourceStartTick + sourceDurationTicks > timedStream.durationTicks) {
|
|
1012
|
+
throw new Error("Insert source range exceeds the registered stream");
|
|
1013
|
+
}
|
|
1014
|
+
return [{
|
|
1015
|
+
kind: "insert-clip",
|
|
1016
|
+
mode: request.mode ?? "insert",
|
|
1017
|
+
placement: {
|
|
1018
|
+
trackId: track.trackId,
|
|
1019
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
1020
|
+
durationFrames: request.duration_frames ?? Math.max(1, sourceTicksToSequenceFrames(sourceDurationTicks, timedStream.timeBase, sequence.frameRate, "ceil")),
|
|
1021
|
+
source: {
|
|
1022
|
+
kind: "timed",
|
|
1023
|
+
artifactId: artifact.value.artifactId,
|
|
1024
|
+
range: {
|
|
1025
|
+
streamId: timedStream.streamId,
|
|
1026
|
+
objectHash: timedStream.objectHash,
|
|
1027
|
+
startTick: sourceStartTick,
|
|
1028
|
+
durationTicks: sourceDurationTicks,
|
|
1029
|
+
timeBase: timedStream.timeBase,
|
|
1030
|
+
},
|
|
1031
|
+
},
|
|
1032
|
+
speed: { numerator: 1, denominator: 1 },
|
|
1033
|
+
reverse: false,
|
|
1034
|
+
audioPolicy: timedStream.kind === "video" ? "mute" : "preserve-pitch",
|
|
1035
|
+
},
|
|
1036
|
+
}];
|
|
1037
|
+
}
|
|
1038
|
+
const manifest = await engine.files.manifest(artifact.value.artifactId);
|
|
1039
|
+
if (!manifest.ok)
|
|
1040
|
+
throw new Error(manifest.error.message);
|
|
1041
|
+
const media = findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
|
|
1042
|
+
if (!media)
|
|
1043
|
+
throw new Error("The artifact has no insertable source media");
|
|
1044
|
+
const track = requiredTrack(sequence, request.track_id, "video");
|
|
1045
|
+
return [{
|
|
1046
|
+
kind: "insert-clip",
|
|
1047
|
+
mode: request.mode ?? "insert",
|
|
1048
|
+
placement: {
|
|
1049
|
+
trackId: track.trackId,
|
|
1050
|
+
timelineStartFrame: request.timeline_start_frame,
|
|
1051
|
+
durationFrames: request.duration_frames ?? 90,
|
|
1052
|
+
source: {
|
|
1053
|
+
kind: "still",
|
|
1054
|
+
artifactId: artifact.value.artifactId,
|
|
1055
|
+
sourcePath: media.name,
|
|
1056
|
+
objectHash: media.objectHash,
|
|
1057
|
+
},
|
|
1058
|
+
},
|
|
1059
|
+
}];
|
|
1060
|
+
}
|
|
1061
|
+
async function buildSequenceEditPlan(engine, input, sourceSurface, actor, expectedRevision) {
|
|
1062
|
+
const sequence = engine.sequences.getPrimary();
|
|
1063
|
+
const contentRevision = sequenceContinuationRevision(sequence);
|
|
1064
|
+
if (expectedRevision
|
|
1065
|
+
&& expectedRevision !== sequence.revision
|
|
1066
|
+
&& expectedRevision !== contentRevision) {
|
|
1067
|
+
throw new Error(`Sequence revision changed: expected ${expectedRevision}, current ${contentRevision}`);
|
|
1068
|
+
}
|
|
1069
|
+
const commandId = uuidv7();
|
|
1070
|
+
const requests = Array.isArray(input) ? input : [input];
|
|
1071
|
+
const appendsOnly = requests.every((request) => request.kind === "append");
|
|
1072
|
+
if (!appendsOnly && requests.some((request) => request.kind === "append")) {
|
|
1073
|
+
throw new Error("Append batches can only contain append requests; plan other edits separately");
|
|
1074
|
+
}
|
|
1075
|
+
const operations = (await Promise.all(requests.map((request) => operationsForRequest(engine, sequence, request)))).flat();
|
|
1076
|
+
if (appendsOnly) {
|
|
1077
|
+
const trackEnds = new Map();
|
|
1078
|
+
for (const operation of operations) {
|
|
1079
|
+
if (operation.kind !== "insert-clip")
|
|
1080
|
+
continue;
|
|
1081
|
+
const placement = operation.placement;
|
|
1082
|
+
placement.timelineStartFrame = trackEnds.get(placement.trackId) ?? placement.timelineStartFrame;
|
|
1083
|
+
trackEnds.set(placement.trackId, placement.timelineStartFrame + placement.durationFrames);
|
|
1084
|
+
}
|
|
1085
|
+
}
|
|
1086
|
+
const confirmationRequired = requests.length > 1 || operations.length > 1;
|
|
1087
|
+
const intent = {
|
|
1088
|
+
intentVersion: MVP_CONTRACT_VERSION,
|
|
1089
|
+
commandId,
|
|
1090
|
+
sequenceId: sequence.sequenceId,
|
|
1091
|
+
baseRevision: engine.head,
|
|
1092
|
+
actor,
|
|
1093
|
+
sourceSurface,
|
|
1094
|
+
confirmationPolicy: "reversible-single-step",
|
|
1095
|
+
operations,
|
|
1096
|
+
};
|
|
1097
|
+
const summary = requests.length === 1
|
|
1098
|
+
? `${requests[0].kind.slice(0, 1).toUpperCase()}${requests[0].kind.slice(1)} sequence edit`
|
|
1099
|
+
: `Apply ${requests.length} ordered sequence edits`;
|
|
1100
|
+
return {
|
|
1101
|
+
planVersion: 1,
|
|
1102
|
+
planId: commandId,
|
|
1103
|
+
sourceSurface,
|
|
1104
|
+
actor,
|
|
1105
|
+
bookId: engine.book.get().bookId,
|
|
1106
|
+
summary,
|
|
1107
|
+
normalizedReferences: [
|
|
1108
|
+
{
|
|
1109
|
+
kind: "sequence",
|
|
1110
|
+
targetId: sequence.sequenceId,
|
|
1111
|
+
revision: engine.head,
|
|
1112
|
+
},
|
|
1113
|
+
],
|
|
1114
|
+
stages: [{
|
|
1115
|
+
stageId: `${commandId}:edit:0`,
|
|
1116
|
+
kind: "edit",
|
|
1117
|
+
summary: requests.map((request) => request.kind).join(" → "),
|
|
1118
|
+
intent,
|
|
1119
|
+
}],
|
|
1120
|
+
confirmation: {
|
|
1121
|
+
required: confirmationRequired,
|
|
1122
|
+
policy: confirmationRequired ? "always" : "reversible-single-step",
|
|
1123
|
+
reason: confirmationRequired
|
|
1124
|
+
? "This plan contains multiple ordered operations"
|
|
1125
|
+
: "One reversible edit can apply immediately",
|
|
1126
|
+
},
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function clipIdFromText(sequence, value) {
|
|
1130
|
+
const token = value.replace(/^@/u, "").toLowerCase();
|
|
1131
|
+
const matches = sequence.clips.filter((clip) => clip.clipId.toLowerCase().startsWith(token));
|
|
1132
|
+
return matches.length === 1 ? matches[0].clipId : null;
|
|
1133
|
+
}
|
|
1134
|
+
function trackIdFromText(sequence, value) {
|
|
1135
|
+
const token = value.replace(/^@/u, "").toLowerCase();
|
|
1136
|
+
const matches = sequence.tracks.filter((track) => track.trackId.toLowerCase().startsWith(token)
|
|
1137
|
+
|| track.name.toLowerCase() === token);
|
|
1138
|
+
return matches.length === 1 ? matches[0].trackId : null;
|
|
1139
|
+
}
|
|
1140
|
+
function integerMatch(text, expression) {
|
|
1141
|
+
const match = expression.exec(text);
|
|
1142
|
+
if (!match)
|
|
1143
|
+
return null;
|
|
1144
|
+
const value = Number(match[1]);
|
|
1145
|
+
return Number.isSafeInteger(value) ? value : null;
|
|
1146
|
+
}
|
|
1147
|
+
async function parseChatEditClause(engine, sequence, clause, mediaContext) {
|
|
1148
|
+
const normalized = clause.trim();
|
|
1149
|
+
if (/\b(insert|add|place)\b/iu.test(normalized)
|
|
1150
|
+
&& !/\bgap\b/iu.test(normalized)) {
|
|
1151
|
+
const reference = scanNotebookMentions(normalized)[0]?.reference;
|
|
1152
|
+
const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1153
|
+
const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
|
|
1154
|
+
if ((!reference && !mediaContext) || startFrame === null) {
|
|
1155
|
+
return "Tell me which @asset to insert and the destination frame.";
|
|
1156
|
+
}
|
|
1157
|
+
if (!reference && mediaContext) {
|
|
1158
|
+
return {
|
|
1159
|
+
kind: "insert",
|
|
1160
|
+
artifact_id: mediaContext.artifactId,
|
|
1161
|
+
timeline_start_frame: startFrame,
|
|
1162
|
+
...(durationFrames === null ? {} : { duration_frames: durationFrames }),
|
|
1163
|
+
};
|
|
1164
|
+
}
|
|
1165
|
+
const resolved = await resolveArtifactRefs(engine, [reference]);
|
|
1166
|
+
if (resolved.unresolved.length > 0) {
|
|
1167
|
+
return `I could not resolve @${reference}. Choose a source from the library.`;
|
|
1168
|
+
}
|
|
1169
|
+
const artifact = resolved.resolved[0];
|
|
1170
|
+
if (!artifact)
|
|
1171
|
+
return `I could not resolve @${reference}.`;
|
|
1172
|
+
return {
|
|
1173
|
+
kind: "insert",
|
|
1174
|
+
artifact_id: artifact.artifactId,
|
|
1175
|
+
timeline_start_frame: startFrame,
|
|
1176
|
+
...(durationFrames === null ? {} : { duration_frames: durationFrames }),
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
if (/\bmove\b/iu.test(normalized)) {
|
|
1180
|
+
const references = normalized.match(/@?[0-9a-f-]{8,}/giu) ?? [];
|
|
1181
|
+
const clipId = references[0]
|
|
1182
|
+
? clipIdFromText(sequence, references[0])
|
|
1183
|
+
: null;
|
|
1184
|
+
const trackId = references[1]
|
|
1185
|
+
? trackIdFromText(sequence, references[1])
|
|
1186
|
+
: null;
|
|
1187
|
+
const startFrame = integerMatch(normalized, /\b(?:at|to|from)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1188
|
+
if (!clipId || !trackId || startFrame === null) {
|
|
1189
|
+
return "Tell me the clip ID, destination track ID, and destination frame.";
|
|
1190
|
+
}
|
|
1191
|
+
return {
|
|
1192
|
+
kind: "move",
|
|
1193
|
+
clip_id: clipId,
|
|
1194
|
+
track_id: trackId,
|
|
1195
|
+
timeline_start_frame: startFrame,
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
if (/\bsplit\b/iu.test(normalized)) {
|
|
1199
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1200
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1201
|
+
const splitFrame = integerMatch(normalized, /\b(?:at|on)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1202
|
+
if (!clipId || splitFrame === null) {
|
|
1203
|
+
return "Tell me the clip ID and sequence frame where it should split.";
|
|
1204
|
+
}
|
|
1205
|
+
return { kind: "split", clip_id: clipId, split_frame: splitFrame };
|
|
1206
|
+
}
|
|
1207
|
+
if (/\b(delete|remove)\b/iu.test(normalized)) {
|
|
1208
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1209
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1210
|
+
if (!clipId)
|
|
1211
|
+
return "Tell me which clip ID to delete.";
|
|
1212
|
+
return {
|
|
1213
|
+
kind: "delete",
|
|
1214
|
+
clip_id: clipId,
|
|
1215
|
+
ripple: /\bripple\b/iu.test(normalized),
|
|
1216
|
+
};
|
|
1217
|
+
}
|
|
1218
|
+
if (/\bgap\b/iu.test(normalized)) {
|
|
1219
|
+
const startFrame = integerMatch(normalized, /\b(?:at|from)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1220
|
+
const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
|
|
1221
|
+
if (startFrame === null || durationFrames === null) {
|
|
1222
|
+
return "Tell me the gap start frame and duration in frames.";
|
|
1223
|
+
}
|
|
1224
|
+
return {
|
|
1225
|
+
kind: "gap",
|
|
1226
|
+
timeline_start_frame: startFrame,
|
|
1227
|
+
duration_frames: durationFrames,
|
|
1228
|
+
};
|
|
1229
|
+
}
|
|
1230
|
+
if (/\b(fit|fill|crop)\b/iu.test(normalized)) {
|
|
1231
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1232
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1233
|
+
const fit = /\bfit\b/iu.test(normalized)
|
|
1234
|
+
? "fit"
|
|
1235
|
+
: /\bcrop\b/iu.test(normalized)
|
|
1236
|
+
? "crop"
|
|
1237
|
+
: "fill";
|
|
1238
|
+
if (!clipId)
|
|
1239
|
+
return "Tell me which clip ID to fit, fill, or crop.";
|
|
1240
|
+
return { kind: "transform", clip_id: clipId, fit };
|
|
1241
|
+
}
|
|
1242
|
+
if (/\breframe\b/iu.test(normalized)) {
|
|
1243
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1244
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1245
|
+
if (!clipId)
|
|
1246
|
+
return "Tell me which visual clip ID to reframe.";
|
|
1247
|
+
return { kind: "reframe", clip_id: clipId };
|
|
1248
|
+
}
|
|
1249
|
+
if (/\breverse\b/iu.test(normalized)) {
|
|
1250
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1251
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1252
|
+
const clip = clipId
|
|
1253
|
+
? sequence.clips.find((candidate) => candidate.clipId === clipId)
|
|
1254
|
+
: null;
|
|
1255
|
+
if (!clip || clip.source.kind !== "timed") {
|
|
1256
|
+
return "Tell me which timed clip ID to reverse.";
|
|
1257
|
+
}
|
|
1258
|
+
const timed = clip;
|
|
1259
|
+
return {
|
|
1260
|
+
kind: "speed",
|
|
1261
|
+
clip_id: clip.clipId,
|
|
1262
|
+
numerator: timed.speed.numerator,
|
|
1263
|
+
denominator: timed.speed.denominator,
|
|
1264
|
+
reverse: !timed.reverse,
|
|
1265
|
+
audio_policy: timed.audioPolicy,
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
if (/\b(dissolve|transition)\b/iu.test(normalized)) {
|
|
1269
|
+
const references = normalized.match(/@?[0-9a-f-]{8,}/giu) ?? [];
|
|
1270
|
+
const outgoing = references[0]
|
|
1271
|
+
? clipIdFromText(sequence, references[0])
|
|
1272
|
+
: null;
|
|
1273
|
+
const incoming = references[1]
|
|
1274
|
+
? clipIdFromText(sequence, references[1])
|
|
1275
|
+
: null;
|
|
1276
|
+
const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
|
|
1277
|
+
if (!outgoing || !incoming) {
|
|
1278
|
+
return "Tell me the outgoing and incoming clip IDs for the transition.";
|
|
1279
|
+
}
|
|
1280
|
+
return {
|
|
1281
|
+
kind: "transition",
|
|
1282
|
+
outgoing_clip_id: outgoing,
|
|
1283
|
+
incoming_clip_id: incoming,
|
|
1284
|
+
transition_kind: /\bdissolve\b/iu.test(normalized) ? "dissolve" : "cut",
|
|
1285
|
+
...(durationFrames === null ? {} : { duration_frames: durationFrames }),
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
if (/\bcaption\b/iu.test(normalized)) {
|
|
1289
|
+
const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1290
|
+
const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
|
|
1291
|
+
const style = /\bhigh-contrast\b/iu.test(normalized)
|
|
1292
|
+
? "high-contrast"
|
|
1293
|
+
: /\bbold\b/iu.test(normalized)
|
|
1294
|
+
? "bold"
|
|
1295
|
+
: "clean";
|
|
1296
|
+
const quoted = /["“]([^"”]+)["”]/u.exec(normalized)?.[1]?.trim();
|
|
1297
|
+
const unquoted = normalized
|
|
1298
|
+
.replace(/^.*?\bcaption\b/iu, "")
|
|
1299
|
+
.replace(/\b(?:at|from|starting\s+at)\s+(?:frame\s+)?\d+\b/giu, "")
|
|
1300
|
+
.replace(/\b(?:for|duration(?:\s+of)?)\s+\d+\s*(?:frames?)?\b/giu, "")
|
|
1301
|
+
.replace(/\b(?:clean|bold|high-contrast)\b/giu, "")
|
|
1302
|
+
.replace(/^[\s:,-]+|[\s:,-]+$/gu, "");
|
|
1303
|
+
const text = quoted ?? unquoted;
|
|
1304
|
+
if (startFrame === null || durationFrames === null || !text) {
|
|
1305
|
+
return "Tell me the caption text, start frame, and duration in frames.";
|
|
1306
|
+
}
|
|
1307
|
+
return {
|
|
1308
|
+
kind: "caption",
|
|
1309
|
+
timeline_start_frame: startFrame,
|
|
1310
|
+
duration_frames: durationFrames,
|
|
1311
|
+
text,
|
|
1312
|
+
style_id: style,
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
if (/\btrim\b/iu.test(normalized)) {
|
|
1316
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1317
|
+
const startFrame = integerMatch(normalized, /\b(?:at|from|starting\s+at)\s+(?:frame\s+)?(\d+)\b/iu);
|
|
1318
|
+
const durationFrames = integerMatch(normalized, /\b(?:for|duration(?:\s+of)?)\s+(\d+)\s*(?:frames?)?\b/iu);
|
|
1319
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1320
|
+
if (!clipId || startFrame === null || durationFrames === null) {
|
|
1321
|
+
return "Tell me the clip ID, destination frame, and duration in frames.";
|
|
1322
|
+
}
|
|
1323
|
+
return {
|
|
1324
|
+
kind: "trim",
|
|
1325
|
+
clip_id: clipId,
|
|
1326
|
+
timeline_start_frame: startFrame,
|
|
1327
|
+
duration_frames: durationFrames,
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
if (/\b(volume|gain|louder|quieter|mute)\b/iu.test(normalized)) {
|
|
1331
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1332
|
+
const gainMatch = /(-?\d+(?:\.\d+)?)\s*(?:db)?\b/iu.exec(normalized.replace(reference ?? "", ""));
|
|
1333
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1334
|
+
const gainDb = /\bmute\b/iu.test(normalized)
|
|
1335
|
+
? -96
|
|
1336
|
+
: gainMatch
|
|
1337
|
+
? Number(gainMatch[1])
|
|
1338
|
+
: Number.NaN;
|
|
1339
|
+
if (!clipId || !Number.isFinite(gainDb) || gainDb < -96 || gainDb > 24) {
|
|
1340
|
+
return "Tell me the audio clip ID and gain from -96 to 24 dB.";
|
|
1341
|
+
}
|
|
1342
|
+
return {
|
|
1343
|
+
kind: "volume",
|
|
1344
|
+
clip_id: clipId,
|
|
1345
|
+
gain_db: gainDb,
|
|
1346
|
+
...(gainDb === -96 ? { muted: true } : {}),
|
|
1347
|
+
};
|
|
1348
|
+
}
|
|
1349
|
+
if (/\bduck\b/iu.test(normalized)) {
|
|
1350
|
+
const reference = /(?:clip\s+)?(@?[0-9a-f-]{8,})/iu.exec(normalized)?.[1];
|
|
1351
|
+
const clipId = reference ? clipIdFromText(sequence, reference) : null;
|
|
1352
|
+
const reduction = /(-\d+(?:\.\d+)?)\s*(?:db)?\b/iu.exec(normalized.replace(reference ?? "", ""));
|
|
1353
|
+
const reductionDb = reduction ? Number(reduction[1]) : -12;
|
|
1354
|
+
if (!clipId || reductionDb < -36 || reductionDb > -1) {
|
|
1355
|
+
return "Tell me the narration clip ID and a reduction from -36 to -1 dB.";
|
|
1356
|
+
}
|
|
1357
|
+
return {
|
|
1358
|
+
kind: "duck",
|
|
1359
|
+
narration_clip_id: clipId,
|
|
1360
|
+
reduction_db: reductionDb,
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
return UNSUPPORTED_CHAT_EDIT_MESSAGE;
|
|
1364
|
+
}
|
|
1365
|
+
async function parseChatEditRequest(engine, prompt, mediaContext) {
|
|
1366
|
+
const clauses = prompt.split(/\s*(?:;|\band\s+then\b|\bthen\b)\s*/iu)
|
|
1367
|
+
.filter((clause) => clause.trim().length > 0);
|
|
1368
|
+
if (clauses.length === 0) {
|
|
1369
|
+
return {
|
|
1370
|
+
kind: "clarification",
|
|
1371
|
+
message: "Describe the edit you want to make.",
|
|
1372
|
+
reason: "incomplete",
|
|
1373
|
+
};
|
|
1374
|
+
}
|
|
1375
|
+
const sequence = engine.sequences.getPrimary();
|
|
1376
|
+
const requests = [];
|
|
1377
|
+
for (const clause of clauses) {
|
|
1378
|
+
const parsed = await parseChatEditClause(engine, sequence, clause, mediaContext);
|
|
1379
|
+
if (typeof parsed === "string") {
|
|
1380
|
+
return {
|
|
1381
|
+
kind: "clarification",
|
|
1382
|
+
message: parsed,
|
|
1383
|
+
reason: parsed === UNSUPPORTED_CHAT_EDIT_MESSAGE
|
|
1384
|
+
? "unsupported"
|
|
1385
|
+
: "incomplete",
|
|
1386
|
+
};
|
|
1387
|
+
}
|
|
1388
|
+
requests.push(parsed);
|
|
1389
|
+
}
|
|
1390
|
+
return { kind: "plan", requests };
|
|
1391
|
+
}
|
|
1392
|
+
export function buildGenerationInsertPlan(engine, input, sourceSurface, actor) {
|
|
1393
|
+
if (input.estimated_cost_usd === undefined
|
|
1394
|
+
&& input.estimate_unavailable !== true) {
|
|
1395
|
+
throw new Error("Generation requires a cost estimate or unavailable warning");
|
|
1396
|
+
}
|
|
1397
|
+
const sequence = engine.sequences.getPrimary();
|
|
1398
|
+
const track = sequence.tracks.find((candidate) => candidate.trackId === input.track_id);
|
|
1399
|
+
if (!track || track.locked || !track.enabled) {
|
|
1400
|
+
throw new Error("Generation destination track is not editable");
|
|
1401
|
+
}
|
|
1402
|
+
const planId = uuidv7();
|
|
1403
|
+
return {
|
|
1404
|
+
planVersion: 1,
|
|
1405
|
+
planId,
|
|
1406
|
+
sourceSurface,
|
|
1407
|
+
actor,
|
|
1408
|
+
bookId: engine.book.get().bookId,
|
|
1409
|
+
summary: `Generate with ${input.model} and insert the result`,
|
|
1410
|
+
normalizedReferences: [{
|
|
1411
|
+
kind: "sequence",
|
|
1412
|
+
targetId: sequence.sequenceId,
|
|
1413
|
+
revision: sequenceContinuationRevision(sequence),
|
|
1414
|
+
}],
|
|
1415
|
+
stages: [{
|
|
1416
|
+
stageId: `${planId}:queued:0`,
|
|
1417
|
+
kind: "queued",
|
|
1418
|
+
summary: `Run ${input.tool}`,
|
|
1419
|
+
jobType: "generation",
|
|
1420
|
+
tool: input.tool,
|
|
1421
|
+
input: input.input,
|
|
1422
|
+
provider: input.provider,
|
|
1423
|
+
model: input.model,
|
|
1424
|
+
...(input.estimated_cost_usd === undefined
|
|
1425
|
+
? { estimateUnavailable: true }
|
|
1426
|
+
: { estimatedCostUsd: input.estimated_cost_usd }),
|
|
1427
|
+
insertsOutput: true,
|
|
1428
|
+
continuation: {
|
|
1429
|
+
sequenceId: sequence.sequenceId,
|
|
1430
|
+
baseRevision: sequenceContinuationRevision(sequence),
|
|
1431
|
+
destination: {
|
|
1432
|
+
trackId: track.trackId,
|
|
1433
|
+
timelineStartFrame: input.timeline_start_frame,
|
|
1434
|
+
...(input.duration_frames === undefined
|
|
1435
|
+
? {}
|
|
1436
|
+
: { durationFrames: input.duration_frames }),
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
}],
|
|
1440
|
+
confirmation: {
|
|
1441
|
+
required: true,
|
|
1442
|
+
policy: "always",
|
|
1443
|
+
reason: "This plan can incur provider spend",
|
|
1444
|
+
},
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
export async function continueQueuedInsert(engine, plan, outputArtifactId, actor) {
|
|
1448
|
+
const stages = Array.isArray(plan.stages) ? plan.stages : [];
|
|
1449
|
+
const stage = stages.find((candidate) => typeof candidate === "object"
|
|
1450
|
+
&& candidate !== null
|
|
1451
|
+
&& candidate.kind === "queued");
|
|
1452
|
+
const continuation = stage && typeof stage.continuation === "object"
|
|
1453
|
+
&& stage.continuation !== null
|
|
1454
|
+
? stage.continuation
|
|
1455
|
+
: null;
|
|
1456
|
+
const destination = continuation
|
|
1457
|
+
&& typeof continuation.destination === "object"
|
|
1458
|
+
&& continuation.destination !== null
|
|
1459
|
+
? continuation.destination
|
|
1460
|
+
: null;
|
|
1461
|
+
const sequenceId = continuation?.sequenceId;
|
|
1462
|
+
const baseRevision = continuation?.baseRevision;
|
|
1463
|
+
if (stage?.insertsOutput !== true
|
|
1464
|
+
|| typeof sequenceId !== "string"
|
|
1465
|
+
|| typeof baseRevision !== "string"
|
|
1466
|
+
|| !destination) {
|
|
1467
|
+
throw new Error("The plan has no revision-pinned queued insertion");
|
|
1468
|
+
}
|
|
1469
|
+
const sequence = engine.sequences.getPrimary();
|
|
1470
|
+
const currentRevision = sequenceContinuationRevision(sequence);
|
|
1471
|
+
if (sequence.sequenceId !== sequenceId || currentRevision !== baseRevision) {
|
|
1472
|
+
return {
|
|
1473
|
+
kind: "stale-continuation",
|
|
1474
|
+
message: "The sequence changed while generation was running. Review a new destination.",
|
|
1475
|
+
expectedRevision: baseRevision,
|
|
1476
|
+
currentRevision,
|
|
1477
|
+
outputArtifactId,
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
const trackId = destination.trackId;
|
|
1481
|
+
const timelineStartFrame = destination.timelineStartFrame;
|
|
1482
|
+
const durationFrames = destination.durationFrames;
|
|
1483
|
+
if (typeof trackId !== "string"
|
|
1484
|
+
|| typeof timelineStartFrame !== "number"
|
|
1485
|
+
|| (durationFrames !== undefined
|
|
1486
|
+
&& typeof durationFrames !== "number")) {
|
|
1487
|
+
throw new Error("The queued insertion destination is invalid");
|
|
1488
|
+
}
|
|
1489
|
+
const next = await buildSequenceEditPlan(engine, {
|
|
1490
|
+
kind: "insert",
|
|
1491
|
+
artifact_id: outputArtifactId,
|
|
1492
|
+
track_id: trackId,
|
|
1493
|
+
timeline_start_frame: timelineStartFrame,
|
|
1494
|
+
...(durationFrames === undefined ? {} : { duration_frames: durationFrames }),
|
|
1495
|
+
}, "system", actor);
|
|
1496
|
+
next.parentActionId = typeof plan.planId === "string" ? plan.planId : undefined;
|
|
1497
|
+
return next;
|
|
1498
|
+
}
|
|
1499
|
+
export function commandPlanDefs(engine) {
|
|
1500
|
+
return [
|
|
1501
|
+
{
|
|
1502
|
+
name: "plan_sequence_edit",
|
|
1503
|
+
description: "Compile one or more sequence edits (insert, trim, move, volume, captions, ...) into one reviewable CommandPlan",
|
|
1504
|
+
scope: "book",
|
|
1505
|
+
schema: {
|
|
1506
|
+
...bookSchema,
|
|
1507
|
+
source_surface: z.enum(["ui", "slash", "chat"]),
|
|
1508
|
+
actor: z.string().min(1),
|
|
1509
|
+
expected_revision: z.string().min(1).optional(),
|
|
1510
|
+
edits: z.array(editRequestSchema).min(1).max(20),
|
|
1511
|
+
},
|
|
1512
|
+
handler: async (args) => {
|
|
1513
|
+
try {
|
|
1514
|
+
return json(await buildSequenceEditPlan(engine, args.edits, args.source_surface, String(args.actor), typeof args.expected_revision === "string"
|
|
1515
|
+
? args.expected_revision
|
|
1516
|
+
: undefined));
|
|
1517
|
+
}
|
|
1518
|
+
catch (error) {
|
|
1519
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
{
|
|
1524
|
+
name: "plan_chat_command",
|
|
1525
|
+
description: "Parse a natural-language sequence edit into a typed CommandPlan without executing tools",
|
|
1526
|
+
scope: "book",
|
|
1527
|
+
schema: {
|
|
1528
|
+
...bookSchema,
|
|
1529
|
+
actor: z.string().min(1),
|
|
1530
|
+
prompt: z.string().min(1),
|
|
1531
|
+
media_context: z.preprocess((value) => {
|
|
1532
|
+
if (!Array.isArray(value))
|
|
1533
|
+
return value;
|
|
1534
|
+
return value.length === 1 ? value[0] : undefined;
|
|
1535
|
+
}, z.object({
|
|
1536
|
+
notebook_id: z.string().uuid(),
|
|
1537
|
+
cell_id: z.string().uuid(),
|
|
1538
|
+
artifact_id: z.string().uuid(),
|
|
1539
|
+
}).optional()).describe("Single fullscreen clip {notebook_id, cell_id, artifact_id}; never an array"),
|
|
1540
|
+
},
|
|
1541
|
+
handler: async (args) => {
|
|
1542
|
+
try {
|
|
1543
|
+
let mediaContext;
|
|
1544
|
+
if (args.media_context) {
|
|
1545
|
+
const context = args.media_context;
|
|
1546
|
+
const notebook = engine.notebooks.read(context.notebook_id);
|
|
1547
|
+
if (!notebook.ok)
|
|
1548
|
+
throw new Error(notebook.error.message);
|
|
1549
|
+
const cell = notebook.value.cells.find((candidate) => candidate.id === context.cell_id);
|
|
1550
|
+
if (!cell || (cell.type !== "image" && cell.type !== "video")) {
|
|
1551
|
+
throw new Error("Fullscreen media context must reference an image or video cell");
|
|
1552
|
+
}
|
|
1553
|
+
if (cell.outputArtifactId !== context.artifact_id) {
|
|
1554
|
+
throw new Error("Fullscreen media context does not match the cell output");
|
|
1555
|
+
}
|
|
1556
|
+
const artifact = engine.artifacts.get(context.artifact_id);
|
|
1557
|
+
if (!artifact.ok)
|
|
1558
|
+
throw new Error(artifact.error.message);
|
|
1559
|
+
mediaContext = { artifactId: artifact.value.artifactId };
|
|
1560
|
+
}
|
|
1561
|
+
const parsed = await parseChatEditRequest(engine, String(args.prompt), mediaContext);
|
|
1562
|
+
if (parsed.kind === "clarification")
|
|
1563
|
+
return json(parsed);
|
|
1564
|
+
return json(await buildSequenceEditPlan(engine, parsed.requests, "chat", String(args.actor)));
|
|
1565
|
+
}
|
|
1566
|
+
catch (error) {
|
|
1567
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1568
|
+
}
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
name: "plan_generation_insert",
|
|
1573
|
+
description: "Plan a paid queued generation with provider, model, cost, and a revision-pinned insertion",
|
|
1574
|
+
scope: "book",
|
|
1575
|
+
schema: {
|
|
1576
|
+
...bookSchema,
|
|
1577
|
+
source_surface: z.enum(["ui", "slash", "chat"]),
|
|
1578
|
+
actor: z.string().min(1),
|
|
1579
|
+
generation: generationPlanSchema,
|
|
1580
|
+
},
|
|
1581
|
+
handler: async (args) => {
|
|
1582
|
+
try {
|
|
1583
|
+
return json(buildGenerationInsertPlan(engine, args.generation, args.source_surface, String(args.actor)));
|
|
1584
|
+
}
|
|
1585
|
+
catch (error) {
|
|
1586
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1587
|
+
}
|
|
1588
|
+
},
|
|
1589
|
+
},
|
|
1590
|
+
{
|
|
1591
|
+
name: "continue_queued_insert",
|
|
1592
|
+
description: "Turn a completed generation into an insertion plan only when its pinned sequence destination is still current",
|
|
1593
|
+
scope: "book",
|
|
1594
|
+
schema: {
|
|
1595
|
+
...bookSchema,
|
|
1596
|
+
plan: z.record(z.string(), z.unknown()),
|
|
1597
|
+
output_artifact_id: z.string().uuid(),
|
|
1598
|
+
actor: z.string().min(1),
|
|
1599
|
+
},
|
|
1600
|
+
handler: async (args) => {
|
|
1601
|
+
try {
|
|
1602
|
+
return json(await continueQueuedInsert(engine, args.plan, String(args.output_artifact_id), String(args.actor)));
|
|
1603
|
+
}
|
|
1604
|
+
catch (error) {
|
|
1605
|
+
return failure(error instanceof Error ? error.message : String(error));
|
|
1606
|
+
}
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
name: "preview_edit_intent",
|
|
1611
|
+
description: "Preview one CommandPlan edit stage through the canonical engine diff",
|
|
1612
|
+
scope: "book",
|
|
1613
|
+
schema: {
|
|
1614
|
+
...bookSchema,
|
|
1615
|
+
intent: intentSchema,
|
|
1616
|
+
},
|
|
1617
|
+
handler: async (args) => {
|
|
1618
|
+
const preview = engine.edits.preview(args.intent);
|
|
1619
|
+
return preview.ok ? json(preview.value) : failure(preview.error.message);
|
|
1620
|
+
},
|
|
1621
|
+
},
|
|
1622
|
+
{
|
|
1623
|
+
name: "commit_edit_intent",
|
|
1624
|
+
description: "Commit an unchanged previewed CommandPlan edit stage atomically",
|
|
1625
|
+
scope: "book",
|
|
1626
|
+
schema: {
|
|
1627
|
+
...bookSchema,
|
|
1628
|
+
intent: intentSchema,
|
|
1629
|
+
preview_hash: z.string().min(1),
|
|
1630
|
+
},
|
|
1631
|
+
handler: async (args) => {
|
|
1632
|
+
const committed = await engine.edits.commit(args.intent, String(args.preview_hash));
|
|
1633
|
+
return committed.ok ? json(committed.value) : failure(committed.error.message);
|
|
1634
|
+
},
|
|
1635
|
+
},
|
|
1636
|
+
{
|
|
1637
|
+
name: "get_edit_action",
|
|
1638
|
+
description: "Read the immutable audit record for a committed CommandPlan edit stage",
|
|
1639
|
+
scope: "book",
|
|
1640
|
+
schema: {
|
|
1641
|
+
...bookSchema,
|
|
1642
|
+
action_id: z.string().uuid(),
|
|
1643
|
+
},
|
|
1644
|
+
handler: async (args) => {
|
|
1645
|
+
const action = engine.edits.get(String(args.action_id));
|
|
1646
|
+
return action.ok ? json(action.value) : failure(action.error.message);
|
|
1647
|
+
},
|
|
1648
|
+
},
|
|
1649
|
+
{
|
|
1650
|
+
name: "restore_edit_action",
|
|
1651
|
+
description: "Create a forward revision restoring a prior edit action or revision",
|
|
1652
|
+
scope: "book",
|
|
1653
|
+
schema: {
|
|
1654
|
+
...bookSchema,
|
|
1655
|
+
target_action_id: z.string().uuid().optional(),
|
|
1656
|
+
target_revision: z.string().min(1).optional(),
|
|
1657
|
+
base_revision: z.string().min(1),
|
|
1658
|
+
actor: z.string().min(1),
|
|
1659
|
+
source_surface: z.enum(["ui", "slash", "chat"]),
|
|
1660
|
+
},
|
|
1661
|
+
handler: async (args) => {
|
|
1662
|
+
if (!args.target_action_id && !args.target_revision) {
|
|
1663
|
+
return failure("A target action or target revision is required");
|
|
1664
|
+
}
|
|
1665
|
+
const restored = await engine.edits.restore({
|
|
1666
|
+
...(typeof args.target_action_id === "string"
|
|
1667
|
+
? { targetActionId: args.target_action_id }
|
|
1668
|
+
: {}),
|
|
1669
|
+
...(typeof args.target_revision === "string"
|
|
1670
|
+
? { targetRevision: args.target_revision }
|
|
1671
|
+
: {}),
|
|
1672
|
+
baseRevision: String(args.base_revision),
|
|
1673
|
+
actor: String(args.actor),
|
|
1674
|
+
sourceSurface: args.source_surface,
|
|
1675
|
+
});
|
|
1676
|
+
return restored.ok ? json(restored.value) : failure(restored.error.message);
|
|
1677
|
+
},
|
|
1678
|
+
},
|
|
1679
|
+
];
|
|
1680
|
+
}
|