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,579 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { srtContent } from "./srt.js";
|
|
5
|
+
import { extractVideoPoster } from "./video-poster.js";
|
|
6
|
+
import { AUDIO_OPTS, H264_MEDIUM, extractWaveform, probeVideo, runFfmpeg, } from "./ffmpeg.js";
|
|
7
|
+
export const MINIMUM_SPLIT_PIECE_SECONDS = 0.02;
|
|
8
|
+
export async function materializeArtifactFile(engine, artifactId, preferredNames) {
|
|
9
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
10
|
+
if (!manifest.ok)
|
|
11
|
+
throw new Error(manifest.error.message);
|
|
12
|
+
const names = manifest.value.files.map((file) => file.name);
|
|
13
|
+
const sourcePath = preferredNames.find((name) => names.includes(name))
|
|
14
|
+
?? names.find((name) => /\.(mp4|mov|mkv|webm|m4a|wav|mp3)$/i.test(name))
|
|
15
|
+
?? names[0];
|
|
16
|
+
if (!sourcePath)
|
|
17
|
+
throw new Error(`Artifact ${artifactId} has no media files`);
|
|
18
|
+
const scratch = await mkdtemp(join(tmpdir(), `vb-media-${artifactId.slice(0, 8)}-`));
|
|
19
|
+
const bytes = await engine.files.read(artifactId, sourcePath);
|
|
20
|
+
if (!bytes.ok)
|
|
21
|
+
throw new Error(bytes.error.message);
|
|
22
|
+
const path = join(scratch, sourcePath.replaceAll("/", "_"));
|
|
23
|
+
await writeFile(path, bytes.value);
|
|
24
|
+
return {
|
|
25
|
+
path,
|
|
26
|
+
sourcePath,
|
|
27
|
+
cleanup: async () => {
|
|
28
|
+
await rm(scratch, { recursive: true, force: true });
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
async function ingestOutput(engine, kind, title, fileName, filePath) {
|
|
33
|
+
const artifact = await engine.artifacts.create({ kind, label: title });
|
|
34
|
+
if (!artifact.ok)
|
|
35
|
+
throw new Error(artifact.error.message);
|
|
36
|
+
const written = await engine.files.writeFromPath(artifact.value.artifactId, fileName, filePath);
|
|
37
|
+
if (!written.ok)
|
|
38
|
+
throw new Error(written.error.message);
|
|
39
|
+
return artifact.value.artifactId;
|
|
40
|
+
}
|
|
41
|
+
export async function registerMediaProbe(engine, artifactId, sourcePath, probe) {
|
|
42
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
43
|
+
if (!manifest.ok)
|
|
44
|
+
throw new Error(manifest.error.message);
|
|
45
|
+
const file = manifest.value.files.find((candidate) => candidate.name === sourcePath);
|
|
46
|
+
if (!file)
|
|
47
|
+
throw new Error(`Output file missing from ${artifactId}: ${sourcePath}`);
|
|
48
|
+
for (const [kind, stream] of [
|
|
49
|
+
["video", probe.videoStream],
|
|
50
|
+
["audio", probe.audioStream],
|
|
51
|
+
]) {
|
|
52
|
+
if (!stream)
|
|
53
|
+
continue;
|
|
54
|
+
const registered = await engine.streams.register({
|
|
55
|
+
artifactId,
|
|
56
|
+
sourcePath,
|
|
57
|
+
objectHash: file.objectHash,
|
|
58
|
+
streamIndex: stream.index,
|
|
59
|
+
kind,
|
|
60
|
+
timeBase: stream.timeBase,
|
|
61
|
+
durationTicks: stream.durationTicks,
|
|
62
|
+
codec: stream.codec,
|
|
63
|
+
...(kind === "video" && stream.video ? { video: stream.video } : {}),
|
|
64
|
+
...(kind === "audio" && stream.audio ? { audio: stream.audio } : {}),
|
|
65
|
+
});
|
|
66
|
+
if (!registered.ok)
|
|
67
|
+
throw new Error(registered.error.message);
|
|
68
|
+
}
|
|
69
|
+
const metadata = await engine.metadata.artifacts.write(artifactId, "original", {
|
|
70
|
+
width: probe.width,
|
|
71
|
+
height: probe.height,
|
|
72
|
+
duration: probe.duration,
|
|
73
|
+
fps: probe.fps,
|
|
74
|
+
hasAudio: probe.hasAudio,
|
|
75
|
+
videoCodec: probe.videoCodec,
|
|
76
|
+
audioCodec: probe.audioCodec,
|
|
77
|
+
});
|
|
78
|
+
if (!metadata.ok)
|
|
79
|
+
throw new Error(metadata.error.message);
|
|
80
|
+
}
|
|
81
|
+
function validateSplitRange(sourceArtifactId, sourceDuration, range) {
|
|
82
|
+
if (!Number.isFinite(range.startSeconds)
|
|
83
|
+
|| !Number.isFinite(range.endSeconds)
|
|
84
|
+
|| range.startSeconds < 0) {
|
|
85
|
+
throw new Error("Split range must use finite, non-negative seconds");
|
|
86
|
+
}
|
|
87
|
+
if (!(range.endSeconds > range.startSeconds)) {
|
|
88
|
+
throw new Error("Split range must have positive duration");
|
|
89
|
+
}
|
|
90
|
+
if (range.startSeconds >= sourceDuration) {
|
|
91
|
+
throw new Error(`Cannot split artifact ${sourceArtifactId}: requested range ${range.startSeconds}-${range.endSeconds}s starts at or after the source video duration of ${sourceDuration}s`);
|
|
92
|
+
}
|
|
93
|
+
return Math.min(range.endSeconds, sourceDuration) - range.startSeconds;
|
|
94
|
+
}
|
|
95
|
+
async function splitMaterializedRange(engine, sourceArtifactId, sourcePath, sourceDuration, range, title) {
|
|
96
|
+
const duration = validateSplitRange(sourceArtifactId, sourceDuration, range);
|
|
97
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-split-"));
|
|
98
|
+
const outPath = join(outDir, "split.mp4");
|
|
99
|
+
try {
|
|
100
|
+
await runFfmpeg([
|
|
101
|
+
"-y",
|
|
102
|
+
"-ss",
|
|
103
|
+
String(range.startSeconds),
|
|
104
|
+
"-i",
|
|
105
|
+
sourcePath,
|
|
106
|
+
"-t",
|
|
107
|
+
String(duration),
|
|
108
|
+
"-map",
|
|
109
|
+
"0:v:0",
|
|
110
|
+
"-map",
|
|
111
|
+
"0:a:0?",
|
|
112
|
+
...H264_MEDIUM,
|
|
113
|
+
...AUDIO_OPTS,
|
|
114
|
+
"-movflags",
|
|
115
|
+
"+faststart",
|
|
116
|
+
outPath,
|
|
117
|
+
]);
|
|
118
|
+
const probe = await probeVideo(outPath);
|
|
119
|
+
if (!probe.videoStream) {
|
|
120
|
+
throw new Error(`Cannot split artifact ${sourceArtifactId}: FFmpeg output has no video stream`);
|
|
121
|
+
}
|
|
122
|
+
const poster = await extractVideoPoster(outPath, probe.videoDuration || probe.duration);
|
|
123
|
+
const artifactId = await ingestOutput(engine, "video", title, "original.mp4", outPath);
|
|
124
|
+
const posterWrite = await engine.files.write(artifactId, "original.jpg", poster);
|
|
125
|
+
if (!posterWrite.ok)
|
|
126
|
+
throw new Error(posterWrite.error.message);
|
|
127
|
+
await registerMediaProbe(engine, artifactId, "original.mp4", probe);
|
|
128
|
+
return {
|
|
129
|
+
artifactId,
|
|
130
|
+
durationSeconds: probe.duration || duration,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
await rm(outDir, { recursive: true, force: true });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
export async function splitSourceRanges(engine, sourceArtifactId, requests, concurrency = 2) {
|
|
138
|
+
if (requests.length === 0)
|
|
139
|
+
throw new Error("At least one split range is required");
|
|
140
|
+
if (!Number.isSafeInteger(concurrency) || concurrency < 1) {
|
|
141
|
+
throw new Error("Split concurrency must be a positive integer");
|
|
142
|
+
}
|
|
143
|
+
const source = await materializeArtifactFile(engine, sourceArtifactId, [
|
|
144
|
+
"original.mp4",
|
|
145
|
+
"original.mov",
|
|
146
|
+
]);
|
|
147
|
+
try {
|
|
148
|
+
const sourceProbe = await probeVideo(source.path);
|
|
149
|
+
if (!sourceProbe.videoStream) {
|
|
150
|
+
throw new Error(`Cannot split artifact ${sourceArtifactId}: source has no video stream`);
|
|
151
|
+
}
|
|
152
|
+
const sourceDuration = sourceProbe.videoDuration || sourceProbe.duration;
|
|
153
|
+
if (!(sourceDuration > 0)) {
|
|
154
|
+
throw new Error(`Cannot split artifact ${sourceArtifactId}: source video duration is unavailable`);
|
|
155
|
+
}
|
|
156
|
+
for (const request of requests) {
|
|
157
|
+
validateSplitRange(sourceArtifactId, sourceDuration, request.range);
|
|
158
|
+
}
|
|
159
|
+
const results = new Array(requests.length);
|
|
160
|
+
let nextIndex = 0;
|
|
161
|
+
let failed = false;
|
|
162
|
+
const worker = async () => {
|
|
163
|
+
while (!failed) {
|
|
164
|
+
const index = nextIndex;
|
|
165
|
+
nextIndex += 1;
|
|
166
|
+
const request = requests[index];
|
|
167
|
+
if (!request)
|
|
168
|
+
return;
|
|
169
|
+
try {
|
|
170
|
+
results[index] = await splitMaterializedRange(engine, sourceArtifactId, source.path, sourceDuration, request.range, request.title);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
failed = true;
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
const settled = await Promise.allSettled(Array.from({ length: Math.min(concurrency, requests.length) }, async () => await worker()));
|
|
179
|
+
const rejected = settled.find((result) => result.status === "rejected");
|
|
180
|
+
if (rejected)
|
|
181
|
+
throw rejected.reason;
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
finally {
|
|
185
|
+
await source.cleanup();
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function buildConcatFilterGraph(ranges, hasAudio) {
|
|
189
|
+
const parts = [];
|
|
190
|
+
ranges.forEach((range, index) => {
|
|
191
|
+
parts.push(`[0:v]trim=start=${range.startSeconds}:end=${range.endSeconds},setpts=PTS-STARTPTS[v${index}]`);
|
|
192
|
+
if (hasAudio) {
|
|
193
|
+
parts.push(`[0:a]atrim=start=${range.startSeconds}:end=${range.endSeconds},asetpts=PTS-STARTPTS[a${index}]`);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
const concatInputs = ranges
|
|
197
|
+
.map((_, index) => (hasAudio ? `[v${index}][a${index}]` : `[v${index}]`))
|
|
198
|
+
.join("");
|
|
199
|
+
const tail = hasAudio ? "[v][a]" : "[v]";
|
|
200
|
+
parts.push(`${concatInputs}concat=n=${ranges.length}:v=1:a=${hasAudio ? 1 : 0}${tail}`);
|
|
201
|
+
return parts.join(";");
|
|
202
|
+
}
|
|
203
|
+
export async function concatSourceRanges(engine, sourceArtifactId, ranges, title) {
|
|
204
|
+
if (ranges.length === 0) {
|
|
205
|
+
throw new Error("At least one concat range is required");
|
|
206
|
+
}
|
|
207
|
+
if (ranges.length === 1) {
|
|
208
|
+
return await splitSourceRange(engine, sourceArtifactId, ranges[0], title);
|
|
209
|
+
}
|
|
210
|
+
const source = await materializeArtifactFile(engine, sourceArtifactId, [
|
|
211
|
+
"original.mp4",
|
|
212
|
+
"original.mov",
|
|
213
|
+
]);
|
|
214
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-concat-"));
|
|
215
|
+
const outPath = join(outDir, "concat.mp4");
|
|
216
|
+
try {
|
|
217
|
+
const probe = await probeVideo(source.path);
|
|
218
|
+
if (!probe.videoStream) {
|
|
219
|
+
throw new Error(`Cannot concat artifact ${sourceArtifactId}: source has no video stream`);
|
|
220
|
+
}
|
|
221
|
+
const sourceDuration = probe.videoDuration || probe.duration;
|
|
222
|
+
for (const range of ranges) {
|
|
223
|
+
validateSplitRange(sourceArtifactId, sourceDuration, range);
|
|
224
|
+
}
|
|
225
|
+
const hasAudio = probe.hasAudio;
|
|
226
|
+
const filter = buildConcatFilterGraph(ranges, hasAudio);
|
|
227
|
+
await runFfmpeg([
|
|
228
|
+
"-y",
|
|
229
|
+
"-i",
|
|
230
|
+
source.path,
|
|
231
|
+
"-filter_complex",
|
|
232
|
+
filter,
|
|
233
|
+
"-map",
|
|
234
|
+
"[v]",
|
|
235
|
+
...(hasAudio ? ["-map", "[a]"] : []),
|
|
236
|
+
...H264_MEDIUM,
|
|
237
|
+
...(hasAudio ? AUDIO_OPTS : []),
|
|
238
|
+
"-movflags",
|
|
239
|
+
"+faststart",
|
|
240
|
+
outPath,
|
|
241
|
+
]);
|
|
242
|
+
const outputProbe = await probeVideo(outPath);
|
|
243
|
+
if (!outputProbe.videoStream) {
|
|
244
|
+
throw new Error(`Cannot concat artifact ${sourceArtifactId}: FFmpeg output has no video stream`);
|
|
245
|
+
}
|
|
246
|
+
const artifactId = await ingestOutput(engine, "video", title, "original.mp4", outPath);
|
|
247
|
+
await registerMediaProbe(engine, artifactId, "original.mp4", outputProbe);
|
|
248
|
+
return {
|
|
249
|
+
artifactId,
|
|
250
|
+
durationSeconds: outputProbe.duration
|
|
251
|
+
|| ranges.reduce((total, range) => total + (range.endSeconds - range.startSeconds), 0),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
finally {
|
|
255
|
+
await source.cleanup();
|
|
256
|
+
await rm(outDir, { recursive: true, force: true });
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
export async function splitSourceRange(engine, sourceArtifactId, range, title) {
|
|
260
|
+
return (await splitSourceRanges(engine, sourceArtifactId, [{ range, title }], 1))[0];
|
|
261
|
+
}
|
|
262
|
+
export function resolveAudioExtractionRange(probe, range) {
|
|
263
|
+
const sourceDuration = probe.audioDuration ?? probe.duration;
|
|
264
|
+
if (!probe.hasAudio || !Number.isFinite(sourceDuration) || sourceDuration <= 0) {
|
|
265
|
+
throw new Error("Source has no playable audio stream");
|
|
266
|
+
}
|
|
267
|
+
const startSeconds = range?.startSeconds ?? 0;
|
|
268
|
+
if (!Number.isFinite(startSeconds) || startSeconds < 0 || startSeconds >= sourceDuration) {
|
|
269
|
+
throw new Error("Audio range must start before the source audio ends");
|
|
270
|
+
}
|
|
271
|
+
const durationSeconds = range?.durationSeconds ?? sourceDuration - startSeconds;
|
|
272
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
273
|
+
throw new Error("Audio range duration must be positive and finite");
|
|
274
|
+
}
|
|
275
|
+
return { startSeconds, durationSeconds: Math.min(durationSeconds, sourceDuration - startSeconds) };
|
|
276
|
+
}
|
|
277
|
+
export async function extractSourceAudio(engine, sourceArtifactId, title, range) {
|
|
278
|
+
const source = await materializeArtifactFile(engine, sourceArtifactId, [
|
|
279
|
+
"original.mp4",
|
|
280
|
+
"original.mov",
|
|
281
|
+
"original.m4a",
|
|
282
|
+
"original.wav",
|
|
283
|
+
]);
|
|
284
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-audio-"));
|
|
285
|
+
const outPath = join(outDir, "audio.m4a");
|
|
286
|
+
try {
|
|
287
|
+
const resolvedRange = resolveAudioExtractionRange(await probeVideo(source.path), range);
|
|
288
|
+
const args = ["-y", "-ss", String(resolvedRange.startSeconds), "-i", source.path,
|
|
289
|
+
"-map", "0:a:0", "-vn", "-t", String(resolvedRange.durationSeconds), ...AUDIO_OPTS, outPath];
|
|
290
|
+
await runFfmpeg(args);
|
|
291
|
+
const probe = await probeVideo(outPath);
|
|
292
|
+
const durationSeconds = probe.audioDuration ?? probe.duration;
|
|
293
|
+
if (!probe.hasAudio || durationSeconds <= 0 || Math.abs(durationSeconds - resolvedRange.durationSeconds) > 0.15) {
|
|
294
|
+
throw new Error("Audio extraction produced an invalid duration");
|
|
295
|
+
}
|
|
296
|
+
const artifactId = await ingestOutput(engine, "audio", title, "original.m4a", outPath);
|
|
297
|
+
await registerMediaProbe(engine, artifactId, "original.m4a", probe);
|
|
298
|
+
const peaks = await extractWaveform(outPath);
|
|
299
|
+
if (peaks.length > 0) {
|
|
300
|
+
const waveform = await engine.metadata.waveforms.write(artifactId, peaks);
|
|
301
|
+
if (!waveform.ok)
|
|
302
|
+
throw new Error(waveform.error.message);
|
|
303
|
+
}
|
|
304
|
+
const existing = await engine.metadata.artifacts.read(artifactId, "original");
|
|
305
|
+
const metadata = await engine.metadata.artifacts.write(artifactId, "original", {
|
|
306
|
+
...(existing.ok ? existing.value : {}),
|
|
307
|
+
origin: "extract_audio",
|
|
308
|
+
sourceArtifactId,
|
|
309
|
+
...(range
|
|
310
|
+
? {
|
|
311
|
+
startSeconds: range.startSeconds,
|
|
312
|
+
...(range.durationSeconds === undefined
|
|
313
|
+
? {}
|
|
314
|
+
: { durationSeconds: range.durationSeconds }),
|
|
315
|
+
}
|
|
316
|
+
: {}),
|
|
317
|
+
});
|
|
318
|
+
if (!metadata.ok)
|
|
319
|
+
throw new Error(metadata.error.message);
|
|
320
|
+
return {
|
|
321
|
+
artifactId,
|
|
322
|
+
durationSeconds,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
finally {
|
|
326
|
+
await source.cleanup();
|
|
327
|
+
await rm(outDir, { recursive: true, force: true });
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
export async function captureFrame(engine, sourceArtifactId, options) {
|
|
331
|
+
const source = await materializeArtifactFile(engine, sourceArtifactId, [
|
|
332
|
+
"original.mp4",
|
|
333
|
+
"original.mov",
|
|
334
|
+
]);
|
|
335
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-frame-"));
|
|
336
|
+
const outPath = join(outDir, "frame.jpg");
|
|
337
|
+
try {
|
|
338
|
+
const probe = await probeVideo(source.path);
|
|
339
|
+
if (!probe.videoStream) {
|
|
340
|
+
throw new Error(`Cannot capture frame from artifact ${sourceArtifactId}: source has no video stream`);
|
|
341
|
+
}
|
|
342
|
+
const start = options.range?.startSeconds ?? 0;
|
|
343
|
+
const sourceEnd = probe.videoDuration || probe.duration;
|
|
344
|
+
if (!(sourceEnd > 0)) {
|
|
345
|
+
throw new Error(`Cannot capture frame from artifact ${sourceArtifactId}: source video duration is unavailable`);
|
|
346
|
+
}
|
|
347
|
+
if (!Number.isFinite(start) || start < 0 || start >= sourceEnd) {
|
|
348
|
+
throw new Error(`Cannot capture frame from artifact ${sourceArtifactId}: requested start ${start}s is outside the source video duration of ${sourceEnd}s`);
|
|
349
|
+
}
|
|
350
|
+
const requestedEnd = options.range?.endSeconds ?? sourceEnd;
|
|
351
|
+
const end = sourceEnd > 0
|
|
352
|
+
? Math.min(requestedEnd, sourceEnd)
|
|
353
|
+
: requestedEnd;
|
|
354
|
+
const requestedTimestamp = options.timestampSeconds;
|
|
355
|
+
if (options.role === "time"
|
|
356
|
+
&& (typeof requestedTimestamp !== "number"
|
|
357
|
+
|| !Number.isFinite(requestedTimestamp))) {
|
|
358
|
+
throw new Error(`Cannot capture frame from artifact ${sourceArtifactId}: requested timestamp ${String(requestedTimestamp)}s is not a finite number`);
|
|
359
|
+
}
|
|
360
|
+
const frameDuration = 1 / Math.max(probe.avgFps ?? probe.fps, 1);
|
|
361
|
+
const lastFrameAt = Math.max(start, end - frameDuration);
|
|
362
|
+
const atSeconds = options.role === "first"
|
|
363
|
+
? start
|
|
364
|
+
: options.role === "time"
|
|
365
|
+
? Math.min(Math.max(requestedTimestamp, start), lastFrameAt)
|
|
366
|
+
: lastFrameAt;
|
|
367
|
+
const attempts = options.role === "last"
|
|
368
|
+
? [...new Set([atSeconds, Math.max(start, atSeconds - frameDuration), start])]
|
|
369
|
+
: [atSeconds];
|
|
370
|
+
let capturedAt = null;
|
|
371
|
+
let captureError;
|
|
372
|
+
for (const candidate of attempts) {
|
|
373
|
+
try {
|
|
374
|
+
await rm(outPath, { force: true });
|
|
375
|
+
await runFfmpeg([
|
|
376
|
+
"-y",
|
|
377
|
+
"-ss",
|
|
378
|
+
String(candidate),
|
|
379
|
+
"-i",
|
|
380
|
+
source.path,
|
|
381
|
+
"-map",
|
|
382
|
+
"0:v:0",
|
|
383
|
+
"-frames:v",
|
|
384
|
+
"1",
|
|
385
|
+
"-q:v",
|
|
386
|
+
"2",
|
|
387
|
+
outPath,
|
|
388
|
+
]);
|
|
389
|
+
const bytes = await readFile(outPath);
|
|
390
|
+
if (bytes.byteLength >= 32) {
|
|
391
|
+
capturedAt = candidate;
|
|
392
|
+
break;
|
|
393
|
+
}
|
|
394
|
+
captureError = new Error("Captured frame is empty");
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
captureError = error;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
if (capturedAt === null) {
|
|
401
|
+
throw captureError instanceof Error
|
|
402
|
+
? captureError
|
|
403
|
+
: new Error("Could not capture a frame from the source video");
|
|
404
|
+
}
|
|
405
|
+
const artifactId = await ingestOutput(engine, "image", options.title, "original.jpg", outPath);
|
|
406
|
+
const metadata = await engine.metadata.artifacts.write(artifactId, "original", {
|
|
407
|
+
origin: "extract_frame",
|
|
408
|
+
sourceArtifactId,
|
|
409
|
+
frameMode: options.role,
|
|
410
|
+
atSeconds: capturedAt,
|
|
411
|
+
});
|
|
412
|
+
if (!metadata.ok)
|
|
413
|
+
throw new Error(metadata.error.message);
|
|
414
|
+
return { artifactId, atSeconds: capturedAt };
|
|
415
|
+
}
|
|
416
|
+
finally {
|
|
417
|
+
await source.cleanup();
|
|
418
|
+
await rm(outDir, { recursive: true, force: true });
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
export async function normalizeSceneVideo(engine, videoArtifactId, audioArtifactId, title, durationSeconds = 15) {
|
|
422
|
+
const video = await materializeArtifactFile(engine, videoArtifactId, [
|
|
423
|
+
"original_audio_replaced.mp4",
|
|
424
|
+
"original.mp4",
|
|
425
|
+
]);
|
|
426
|
+
const audio = await materializeArtifactFile(engine, audioArtifactId, [
|
|
427
|
+
"original.m4a",
|
|
428
|
+
"original.wav",
|
|
429
|
+
"original.mp3",
|
|
430
|
+
"original.mp4",
|
|
431
|
+
]);
|
|
432
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-normalize-scene-"));
|
|
433
|
+
const outPath = join(outDir, "scene.mp4");
|
|
434
|
+
try {
|
|
435
|
+
await runFfmpeg([
|
|
436
|
+
"-y",
|
|
437
|
+
"-i",
|
|
438
|
+
video.path,
|
|
439
|
+
"-i",
|
|
440
|
+
audio.path,
|
|
441
|
+
"-filter_complex",
|
|
442
|
+
`[0:v]setpts=PTS-STARTPTS,tpad=stop_mode=clone:stop_duration=${durationSeconds},scale=1280:720:force_original_aspect_ratio=decrease,pad=1280:720:(ow-iw)/2:(oh-ih)/2,setsar=1,fps=30[v];[1:a]atrim=duration=${durationSeconds},asetpts=PTS-STARTPTS,apad=pad_dur=${durationSeconds}[a]`,
|
|
443
|
+
"-map",
|
|
444
|
+
"[v]",
|
|
445
|
+
"-map",
|
|
446
|
+
"[a]",
|
|
447
|
+
...H264_MEDIUM,
|
|
448
|
+
...AUDIO_OPTS,
|
|
449
|
+
"-t",
|
|
450
|
+
String(durationSeconds),
|
|
451
|
+
"-movflags",
|
|
452
|
+
"+faststart",
|
|
453
|
+
outPath,
|
|
454
|
+
]);
|
|
455
|
+
const probe = await probeVideo(outPath);
|
|
456
|
+
const artifactId = await ingestOutput(engine, "video", title, "original.mp4", outPath);
|
|
457
|
+
await registerMediaProbe(engine, artifactId, "original.mp4", probe);
|
|
458
|
+
return {
|
|
459
|
+
artifactId,
|
|
460
|
+
durationSeconds: probe.duration || durationSeconds,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
finally {
|
|
464
|
+
await video.cleanup();
|
|
465
|
+
await audio.cleanup();
|
|
466
|
+
await rm(outDir, { recursive: true, force: true });
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function burnInSrt(captions) {
|
|
470
|
+
return srtContent(captions.map((caption) => ({
|
|
471
|
+
startSeconds: caption.startSeconds,
|
|
472
|
+
endSeconds: caption.startSeconds + caption.durationSeconds,
|
|
473
|
+
text: caption.text,
|
|
474
|
+
})));
|
|
475
|
+
}
|
|
476
|
+
function subtitleFilterPath(path) {
|
|
477
|
+
return path
|
|
478
|
+
.replaceAll("\\", "\\\\")
|
|
479
|
+
.replaceAll(":", "\\:")
|
|
480
|
+
.replaceAll("'", "\\'");
|
|
481
|
+
}
|
|
482
|
+
export async function exportSequenceMaster(engine, clipArtifactIds, audioArtifactId, title, options = {}) {
|
|
483
|
+
if (clipArtifactIds.length === 0) {
|
|
484
|
+
throw new Error("Export requires at least one clip");
|
|
485
|
+
}
|
|
486
|
+
const outDir = await mkdtemp(join(tmpdir(), "vb-export-"));
|
|
487
|
+
const listPath = join(outDir, "concat.txt");
|
|
488
|
+
const concatPath = join(outDir, "picture.mp4");
|
|
489
|
+
const finalPath = join(outDir, "master.mp4");
|
|
490
|
+
const captionsPath = join(outDir, "captions.srt");
|
|
491
|
+
const cleanups = [];
|
|
492
|
+
try {
|
|
493
|
+
const lines = [];
|
|
494
|
+
for (const clipId of clipArtifactIds) {
|
|
495
|
+
const clip = await materializeArtifactFile(engine, clipId, ["original.mp4"]);
|
|
496
|
+
cleanups.push(clip.cleanup);
|
|
497
|
+
lines.push(`file '${clip.path.replaceAll("'", "'\\''")}'`);
|
|
498
|
+
}
|
|
499
|
+
await writeFile(listPath, `${lines.join("\n")}\n`);
|
|
500
|
+
await runFfmpeg([
|
|
501
|
+
"-y",
|
|
502
|
+
"-f",
|
|
503
|
+
"concat",
|
|
504
|
+
"-safe",
|
|
505
|
+
"0",
|
|
506
|
+
"-i",
|
|
507
|
+
listPath,
|
|
508
|
+
...H264_MEDIUM,
|
|
509
|
+
"-an",
|
|
510
|
+
concatPath,
|
|
511
|
+
]);
|
|
512
|
+
const captions = options.captions ?? [];
|
|
513
|
+
if (captions.length > 0) {
|
|
514
|
+
await writeFile(captionsPath, burnInSrt(captions));
|
|
515
|
+
}
|
|
516
|
+
const videoFilter = captions.length > 0
|
|
517
|
+
? [
|
|
518
|
+
"-vf",
|
|
519
|
+
`subtitles='${subtitleFilterPath(captionsPath)}':force_style='FontName=Arial,FontSize=28,PrimaryColour=&H00FFFFFF,OutlineColour=&H00101010,BorderStyle=1,Outline=2,Shadow=0,Alignment=2,MarginV=42'`,
|
|
520
|
+
]
|
|
521
|
+
: [];
|
|
522
|
+
if (audioArtifactId) {
|
|
523
|
+
const audio = await materializeArtifactFile(engine, audioArtifactId, [
|
|
524
|
+
"original.m4a",
|
|
525
|
+
"original.wav",
|
|
526
|
+
"original.mp4",
|
|
527
|
+
]);
|
|
528
|
+
cleanups.push(audio.cleanup);
|
|
529
|
+
await runFfmpeg([
|
|
530
|
+
"-y",
|
|
531
|
+
"-i",
|
|
532
|
+
concatPath,
|
|
533
|
+
"-i",
|
|
534
|
+
audio.path,
|
|
535
|
+
"-map",
|
|
536
|
+
"0:v:0",
|
|
537
|
+
"-map",
|
|
538
|
+
"1:a:0?",
|
|
539
|
+
...videoFilter,
|
|
540
|
+
...H264_MEDIUM,
|
|
541
|
+
...AUDIO_OPTS,
|
|
542
|
+
"-shortest",
|
|
543
|
+
"-movflags",
|
|
544
|
+
"+faststart",
|
|
545
|
+
finalPath,
|
|
546
|
+
]);
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
await runFfmpeg([
|
|
550
|
+
"-y",
|
|
551
|
+
"-i",
|
|
552
|
+
concatPath,
|
|
553
|
+
"-f",
|
|
554
|
+
"lavfi",
|
|
555
|
+
"-i",
|
|
556
|
+
"anullsrc=channel_layout=stereo:sample_rate=48000",
|
|
557
|
+
"-map",
|
|
558
|
+
"0:v:0",
|
|
559
|
+
"-map",
|
|
560
|
+
"1:a:0",
|
|
561
|
+
...videoFilter,
|
|
562
|
+
...H264_MEDIUM,
|
|
563
|
+
...AUDIO_OPTS,
|
|
564
|
+
"-shortest",
|
|
565
|
+
"-movflags",
|
|
566
|
+
"+faststart",
|
|
567
|
+
finalPath,
|
|
568
|
+
]);
|
|
569
|
+
}
|
|
570
|
+
const probe = await probeVideo(finalPath);
|
|
571
|
+
const artifactId = await ingestOutput(engine, "final", title, "original.mp4", finalPath);
|
|
572
|
+
await registerMediaProbe(engine, artifactId, "original.mp4", probe);
|
|
573
|
+
return { artifactId };
|
|
574
|
+
}
|
|
575
|
+
finally {
|
|
576
|
+
await Promise.all(cleanups.map((cleanup) => cleanup()));
|
|
577
|
+
await rm(outDir, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { normalizeLegacyExtractCells } from "./notebook-cell-outputs.js";
|
|
2
|
+
const writeChains = new Map();
|
|
3
|
+
export async function withNotebookMutation(notebookId, mutate) {
|
|
4
|
+
const current = writeChains.get(notebookId) ?? Promise.resolve();
|
|
5
|
+
const next = current.then(mutate);
|
|
6
|
+
writeChains.set(notebookId, next.catch(() => { }));
|
|
7
|
+
return await next;
|
|
8
|
+
}
|
|
9
|
+
export async function mutateNotebook(engine, notebookId, operation, mutate, details = {}, options = {}) {
|
|
10
|
+
return await withNotebookMutation(notebookId, async () => {
|
|
11
|
+
const read = engine.notebooks.read(notebookId);
|
|
12
|
+
if (!read.ok)
|
|
13
|
+
return read;
|
|
14
|
+
return await engine.notebooks.write(mutate(normalizeLegacyExtractCells(read.value)), {
|
|
15
|
+
operation,
|
|
16
|
+
details,
|
|
17
|
+
...(options.artifactId ? { artifactId: options.artifactId } : {}),
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { nextHorizontalSlotFrom, nextVerticalSlotFrom, NOTEBOOK_GRID_COLUMN_COUNT, NOTEBOOK_GRID_ROW_COUNT, } from "videobook-engine";
|
|
2
|
+
export function slotBelow(slot) {
|
|
3
|
+
if (slot.row + 1 < NOTEBOOK_GRID_ROW_COUNT) {
|
|
4
|
+
return { row: slot.row + 1, column: slot.column };
|
|
5
|
+
}
|
|
6
|
+
return {
|
|
7
|
+
row: slot.row,
|
|
8
|
+
column: Math.min(slot.column + 1, NOTEBOOK_GRID_COLUMN_COUNT - 1),
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function slotRight(slot) {
|
|
12
|
+
if (slot.column + 1 < NOTEBOOK_GRID_COLUMN_COUNT) {
|
|
13
|
+
return { row: slot.row, column: slot.column + 1 };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
row: Math.min(slot.row + 1, NOTEBOOK_GRID_ROW_COUNT - 1),
|
|
17
|
+
column: slot.column,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function lastCreatedCellSlot(cells) {
|
|
21
|
+
if (cells.length === 0)
|
|
22
|
+
return null;
|
|
23
|
+
let latest = cells[0];
|
|
24
|
+
for (const cell of cells) {
|
|
25
|
+
if (cell.id > latest.id)
|
|
26
|
+
latest = cell;
|
|
27
|
+
}
|
|
28
|
+
return latest.slot;
|
|
29
|
+
}
|
|
30
|
+
export function isVerticalGenerationType(type) {
|
|
31
|
+
return type === "generate_video"
|
|
32
|
+
|| type === "split_video"
|
|
33
|
+
|| type === "video"
|
|
34
|
+
|| type === "concat";
|
|
35
|
+
}
|
|
36
|
+
export function nextGenerationOutputSlot(producerType, producerSlot, occupied) {
|
|
37
|
+
if (producerType === "generate_video") {
|
|
38
|
+
return nextVerticalSlotFrom(slotBelow(producerSlot), occupied);
|
|
39
|
+
}
|
|
40
|
+
return nextHorizontalSlotFrom(slotRight(producerSlot), occupied);
|
|
41
|
+
}
|