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,98 @@
|
|
|
1
|
+
import { findVideoFile, MVP_CONTRACT_VERSION, sourceTicksToSequenceFrames } from "videobook-engine";
|
|
2
|
+
import { v7 as uuidv7 } from "uuid";
|
|
3
|
+
import { registerJobHandler } from "../queue.js";
|
|
4
|
+
import { readGoal, saveGoal } from "../goal-store.js";
|
|
5
|
+
import { withNotebookMutation } from "../notebook-mutations.js";
|
|
6
|
+
import { fittedTransform } from "../rhythm/visual-fit.js";
|
|
7
|
+
export async function appendGoalClips(engine, goal, artifactIds) {
|
|
8
|
+
if (artifactIds.length === 0 || goal.clipIds.length !== artifactIds.length) {
|
|
9
|
+
throw new Error("The finished clips do not match the reviewed goal");
|
|
10
|
+
}
|
|
11
|
+
const sequence = engine.sequences.getPrimary();
|
|
12
|
+
const existing = sequence.clips.filter((clip) => goal.clipIds.includes(clip.clipId));
|
|
13
|
+
const snapshot = JSON.stringify(existing);
|
|
14
|
+
const unchangedSources = existing.length === artifactIds.length && goal.clipIds.every((id, index) => existing.find(clip => clip.clipId === id)?.source.artifactId === artifactIds[index]);
|
|
15
|
+
if (existing.length) {
|
|
16
|
+
if (existing.length !== artifactIds.length)
|
|
17
|
+
throw new Error("Some goal clips were already edited or removed. Review the sequence before assembling again.");
|
|
18
|
+
if (unchangedSources)
|
|
19
|
+
return {
|
|
20
|
+
sequenceId: sequence.sequenceId,
|
|
21
|
+
startFrame: Math.min(...existing.map((clip) => clip.timelineStartFrame)),
|
|
22
|
+
durationFrames: existing.reduce((sum, clip) => sum + clip.durationFrames, 0),
|
|
23
|
+
clipSnapshot: goal.assembly?.clipSnapshot ?? snapshot, artifactIds,
|
|
24
|
+
};
|
|
25
|
+
if (!goal.assembly?.clipSnapshot || goal.assembly.clipSnapshot !== snapshot)
|
|
26
|
+
throw new Error("The goal's timeline clips were edited. Keep those edits and place the revised videos manually.");
|
|
27
|
+
}
|
|
28
|
+
if (goal.assembly && !existing.length)
|
|
29
|
+
throw new Error("This goal was already assembled; its clips have since been removed");
|
|
30
|
+
const track = sequence.tracks.find((candidate) => candidate.trackId === goal.trackId);
|
|
31
|
+
if (!track || track.kind !== "video" || track.locked || !track.enabled) {
|
|
32
|
+
throw new Error("The goal's video track is no longer editable. Unlock or enable it, then assemble again.");
|
|
33
|
+
}
|
|
34
|
+
const startFrame = existing.length ? Math.min(...existing.map(clip => clip.timelineStartFrame)) : Math.max(0, ...sequence.clips.map((clip) => clip.timelineStartFrame + clip.durationFrames), ...sequence.captions.map((caption) => caption.timelineStartFrame + caption.durationFrames));
|
|
35
|
+
let cursor = startFrame;
|
|
36
|
+
const operations = existing.map(clip => ({ kind: "remove-clip", clipId: clip.clipId, ripple: false }));
|
|
37
|
+
for (const [index, artifactId] of artifactIds.entries()) {
|
|
38
|
+
const status = await engine.status.get(artifactId);
|
|
39
|
+
if (!status.ok || status.value !== "ready")
|
|
40
|
+
throw new Error(`Shot ${index + 1} is still processing`);
|
|
41
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
42
|
+
if (!manifest.ok)
|
|
43
|
+
throw new Error(manifest.error.message);
|
|
44
|
+
const file = findVideoFile(manifest.value.files);
|
|
45
|
+
const streams = engine.streams.list(artifactId);
|
|
46
|
+
if (!streams.ok)
|
|
47
|
+
throw new Error(streams.error.message);
|
|
48
|
+
const stream = streams.value.find((candidate) => candidate.kind === "video"
|
|
49
|
+
&& candidate.objectHash === file?.objectHash && candidate.sourcePath === file?.name);
|
|
50
|
+
if (!stream)
|
|
51
|
+
throw new Error(`Shot ${index + 1} has no playable video stream`);
|
|
52
|
+
const durationFrames = sourceTicksToSequenceFrames(stream.durationTicks, stream.timeBase, sequence.frameRate, "floor");
|
|
53
|
+
if (durationFrames < 1)
|
|
54
|
+
throw new Error(`Shot ${index + 1} has no playable frames`);
|
|
55
|
+
operations.push({
|
|
56
|
+
kind: "insert-clip", mode: "overwrite", clipId: goal.clipIds[index],
|
|
57
|
+
placement: {
|
|
58
|
+
trackId: track.trackId, timelineStartFrame: cursor, durationFrames,
|
|
59
|
+
source: { kind: "timed", artifactId, range: {
|
|
60
|
+
streamId: stream.streamId, objectHash: stream.objectHash,
|
|
61
|
+
startTick: 0, durationTicks: stream.durationTicks, timeBase: stream.timeBase,
|
|
62
|
+
} },
|
|
63
|
+
speed: { numerator: 1, denominator: 1 }, reverse: false, audioPolicy: "preserve-pitch",
|
|
64
|
+
audio: { gainDb: 0, muted: false, fadeInFrames: 0, fadeOutFrames: 0 },
|
|
65
|
+
transform: fittedTransform("fit"),
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
cursor += durationFrames;
|
|
69
|
+
}
|
|
70
|
+
if (sequence.clips.some(clip => !goal.clipIds.includes(clip.clipId) && clip.trackId === track.trackId && clip.timelineStartFrame < cursor && clip.timelineStartFrame + clip.durationFrames > startFrame)) {
|
|
71
|
+
throw new Error("The revised clips would overlap other timeline edits. Place the revised videos manually.");
|
|
72
|
+
}
|
|
73
|
+
const intent = {
|
|
74
|
+
intentVersion: MVP_CONTRACT_VERSION, commandId: uuidv7(), sequenceId: sequence.sequenceId,
|
|
75
|
+
baseRevision: sequence.revision, actor: "goal", sourceSurface: "system",
|
|
76
|
+
confirmationPolicy: "reversible-single-step", operations,
|
|
77
|
+
};
|
|
78
|
+
const preview = engine.edits.preview(intent);
|
|
79
|
+
if (!preview.ok)
|
|
80
|
+
throw new Error(preview.error.message);
|
|
81
|
+
const committed = await engine.edits.commit(intent, preview.value.previewHash);
|
|
82
|
+
if (!committed.ok)
|
|
83
|
+
throw new Error(committed.error.message);
|
|
84
|
+
return { sequenceId: sequence.sequenceId, startFrame, durationFrames: cursor - startFrame, artifactIds, clipSnapshot: JSON.stringify(engine.sequences.getPrimary().clips.filter(clip => goal.clipIds.includes(clip.clipId))) };
|
|
85
|
+
}
|
|
86
|
+
export function registerAssembleGoalJob(engine) {
|
|
87
|
+
registerJobHandler("assemble_goal", async (payload, context) => {
|
|
88
|
+
const goalId = String(payload.goalId);
|
|
89
|
+
return withNotebookMutation(`goal:${goalId}`, async () => {
|
|
90
|
+
const goal = readGoal(engine, goalId);
|
|
91
|
+
if (context.signal.aborted || goal.status === "cancelled")
|
|
92
|
+
throw new Error("Goal cancelled");
|
|
93
|
+
const assembly = await appendGoalClips(engine, goal, payload.artifactIds);
|
|
94
|
+
saveGoal(engine, { ...readGoal(engine, goalId), assembly });
|
|
95
|
+
return { sequence_id: assembly.sequenceId, status: "completed" };
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, extname, join } from "node:path";
|
|
4
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
5
|
+
import { extractWaveform, probeVideo, runFfmpeg } from "../ffmpeg.js";
|
|
6
|
+
import { audioOnlyOutputOptions, videoAudioOutputOptions } from "../audio-encoding.js";
|
|
7
|
+
import { registerMediaStreams } from "./process-video.js";
|
|
8
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
9
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
10
|
+
const LOCK_DURATION_MS = 300_000;
|
|
11
|
+
function buildBoostOutputOptions(boost, isVideo, inputPath) {
|
|
12
|
+
const audioFilter = `volume=${boost}`;
|
|
13
|
+
if (isVideo) {
|
|
14
|
+
return ["-c:v", "copy", "-af", audioFilter, ...videoAudioOutputOptions(inputPath)];
|
|
15
|
+
}
|
|
16
|
+
return ["-af", audioFilter, ...audioOnlyOutputOptions(inputPath)];
|
|
17
|
+
}
|
|
18
|
+
export function registerAudioBoostJob(engine) {
|
|
19
|
+
registerJobHandler("audio_boost", async (payload, ctx) => {
|
|
20
|
+
const { inputPath, boost, isVideo, assetId, assetDir, bookId, } = payload;
|
|
21
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "apply_sfx", { durationMs: LOCK_DURATION_MS, data: { state: "boosting-audio" } }, async () => {
|
|
22
|
+
const inputFilename = basename(inputPath);
|
|
23
|
+
const mediaPath = join(assetDir, inputFilename);
|
|
24
|
+
const workspace = await mkdtemp(join(tmpdir(), "videobook-audio-boost-"));
|
|
25
|
+
const outputPath = join(workspace, `output${extname(inputFilename)}`);
|
|
26
|
+
try {
|
|
27
|
+
const source = await probeVideo(inputPath, { signal: ctx.signal });
|
|
28
|
+
if (!source.hasAudio)
|
|
29
|
+
throw new Error("Source has no playable audio stream");
|
|
30
|
+
await runFfmpeg(["-y", "-i", inputPath, ...buildBoostOutputOptions(boost, isVideo, inputPath), outputPath], { signal: ctx.signal });
|
|
31
|
+
const output = await probeVideo(outputPath, { signal: ctx.signal });
|
|
32
|
+
if (!output.hasAudio || Math.abs(output.duration - source.duration) > 0.15) {
|
|
33
|
+
throw new Error("Audio boost produced an invalid duration");
|
|
34
|
+
}
|
|
35
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
36
|
+
if (!isVideo) {
|
|
37
|
+
await registerMediaStreams(engine, assetId, inputFilename, output);
|
|
38
|
+
const previous = await engine.metadata.artifacts.read(assetId, "original");
|
|
39
|
+
const metadata = await engine.metadata.artifacts.write(assetId, "original", {
|
|
40
|
+
...(previous.ok ? previous.value : {}),
|
|
41
|
+
origin: "audio_boost",
|
|
42
|
+
boost,
|
|
43
|
+
duration: output.audioDuration ?? output.duration,
|
|
44
|
+
});
|
|
45
|
+
if (!metadata.ok)
|
|
46
|
+
throw new Error(metadata.error.message);
|
|
47
|
+
const waveform = await engine.metadata.waveforms.write(assetId, await extractWaveform(mediaPath));
|
|
48
|
+
if (!waveform.ok)
|
|
49
|
+
throw new Error(waveform.error.message);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
finally {
|
|
53
|
+
await rm(workspace, { recursive: true, force: true });
|
|
54
|
+
}
|
|
55
|
+
return { mediaPath };
|
|
56
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
57
|
+
if (isVideo) {
|
|
58
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
59
|
+
type: "process_video",
|
|
60
|
+
videoPath: result.mediaPath,
|
|
61
|
+
assetId,
|
|
62
|
+
assetDir,
|
|
63
|
+
bookId,
|
|
64
|
+
provenance: { origin: "audio_boost", boost, boosted_at: new Date().toISOString() },
|
|
65
|
+
force: true,
|
|
66
|
+
skipDownstreamJobs: true,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return { success: true };
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
import { extractWaveform, probeVideo, runFfmpeg } from "../ffmpeg.js";
|
|
5
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
6
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
7
|
+
import { audioOnlyOutputOptions, videoAudioOutputOptions } from "../audio-encoding.js";
|
|
8
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
9
|
+
import { registerMediaStreams } from "./process-video.js";
|
|
10
|
+
const LOCK_DURATION_MS = 30 * 60_000;
|
|
11
|
+
const STALE_DERIVATIVES = [
|
|
12
|
+
".original.analysis.json",
|
|
13
|
+
".analysis.gpt4otrans.json",
|
|
14
|
+
"elevenlabs.json",
|
|
15
|
+
".suggest-sfx.json",
|
|
16
|
+
];
|
|
17
|
+
function decimal(value) {
|
|
18
|
+
return value.toFixed(6).replace(/0+$/, "").replace(/\.$/, "");
|
|
19
|
+
}
|
|
20
|
+
function outputContainerOptions(outputPath) {
|
|
21
|
+
const extension = extname(outputPath).toLowerCase();
|
|
22
|
+
return extension === ".mp4" || extension === ".mov" || extension === ".m4a"
|
|
23
|
+
? ["-movflags", "+faststart"]
|
|
24
|
+
: [];
|
|
25
|
+
}
|
|
26
|
+
export function buildAudioMixFilter(targetHasAudio, targetDurationSeconds, layerVolumePercent) {
|
|
27
|
+
if (!Number.isFinite(targetDurationSeconds) || targetDurationSeconds <= 0) {
|
|
28
|
+
throw new Error(`Invalid target duration: ${targetDurationSeconds}`);
|
|
29
|
+
}
|
|
30
|
+
if (!Number.isFinite(layerVolumePercent) || layerVolumePercent < 0 || layerVolumePercent > 100) {
|
|
31
|
+
throw new Error(`Layer volume must be between 0 and 100, got: ${layerVolumePercent}`);
|
|
32
|
+
}
|
|
33
|
+
const duration = decimal(targetDurationSeconds);
|
|
34
|
+
const volume = decimal(layerVolumePercent / 100);
|
|
35
|
+
const base = targetHasAudio
|
|
36
|
+
? `[0:a:0]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,apad,atrim=duration=${duration},asetpts=PTS-STARTPTS[base]`
|
|
37
|
+
: `anullsrc=channel_layout=stereo:sample_rate=48000,atrim=duration=${duration},asetpts=PTS-STARTPTS[base]`;
|
|
38
|
+
const layer = `[1:a:0]aresample=48000,aformat=sample_fmts=fltp:channel_layouts=stereo,volume=${volume},apad,atrim=duration=${duration},asetpts=PTS-STARTPTS[layer]`;
|
|
39
|
+
const mix = "[base][layer]amix=inputs=2:duration=first:dropout_transition=0:normalize=0,alimiter=limit=0.95:level=false[outa]";
|
|
40
|
+
return `${base};${layer};${mix}`;
|
|
41
|
+
}
|
|
42
|
+
export async function mixAudioMedia(targetPath, layerPath, outputPath, targetKind, layerVolumePercent, signal) {
|
|
43
|
+
const [targetProbe, layerProbe] = await Promise.all([
|
|
44
|
+
probeVideo(targetPath, { signal }),
|
|
45
|
+
probeVideo(layerPath, { signal }),
|
|
46
|
+
]);
|
|
47
|
+
if (!layerProbe.hasAudio)
|
|
48
|
+
throw new Error("Layer asset has no audio stream");
|
|
49
|
+
if (targetKind === "video" && targetProbe.videoCodec === null) {
|
|
50
|
+
throw new Error("Video target has no video stream");
|
|
51
|
+
}
|
|
52
|
+
if (targetKind === "audio" && !targetProbe.hasAudio) {
|
|
53
|
+
throw new Error("Audio target has no audio stream");
|
|
54
|
+
}
|
|
55
|
+
const targetDuration = targetKind === "video"
|
|
56
|
+
? (targetProbe.videoDuration > 0 ? targetProbe.videoDuration : targetProbe.duration)
|
|
57
|
+
: (targetProbe.audioDuration ?? targetProbe.duration);
|
|
58
|
+
const filter = buildAudioMixFilter(targetProbe.hasAudio, targetDuration, layerVolumePercent);
|
|
59
|
+
const args = [
|
|
60
|
+
"-y",
|
|
61
|
+
"-i", targetPath,
|
|
62
|
+
"-i", layerPath,
|
|
63
|
+
"-filter_complex", filter,
|
|
64
|
+
];
|
|
65
|
+
if (targetKind === "video") {
|
|
66
|
+
args.push("-map", "0:v:0", "-map", "[outa]", "-map_metadata", "0", "-map_chapters", "0", "-c:v", "copy", ...videoAudioOutputOptions(outputPath), ...outputContainerOptions(outputPath), outputPath);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
args.push("-map", "[outa]", ...audioOnlyOutputOptions(outputPath), ...outputContainerOptions(outputPath), outputPath);
|
|
70
|
+
}
|
|
71
|
+
await runFfmpeg(args, { signal });
|
|
72
|
+
const outputProbe = await probeVideo(outputPath, { signal });
|
|
73
|
+
if (!outputProbe.hasAudio)
|
|
74
|
+
throw new Error("Audio mix output has no audio stream");
|
|
75
|
+
if (targetKind === "video") {
|
|
76
|
+
if (outputProbe.videoCodec === null)
|
|
77
|
+
throw new Error("Audio mix output has no video stream");
|
|
78
|
+
if (outputProbe.videoCodec !== targetProbe.videoCodec) {
|
|
79
|
+
throw new Error(`Audio mix changed the video codec from ${targetProbe.videoCodec} to ${outputProbe.videoCodec}`);
|
|
80
|
+
}
|
|
81
|
+
if (Math.abs(outputProbe.videoDuration - targetDuration) > 0.5) {
|
|
82
|
+
throw new Error(`Audio mix changed video duration from ${targetDuration.toFixed(3)}s to ${outputProbe.videoDuration.toFixed(3)}s`);
|
|
83
|
+
}
|
|
84
|
+
return outputProbe;
|
|
85
|
+
}
|
|
86
|
+
const outputDuration = outputProbe.audioDuration ?? outputProbe.duration;
|
|
87
|
+
if (Math.abs(outputDuration - targetDuration) > 0.5) {
|
|
88
|
+
throw new Error(`Audio mix changed audio duration from ${targetDuration.toFixed(3)}s to ${outputDuration.toFixed(3)}s`);
|
|
89
|
+
}
|
|
90
|
+
return outputProbe;
|
|
91
|
+
}
|
|
92
|
+
async function deleteIfPresent(engine, assetId, filename) {
|
|
93
|
+
const result = await engine.files.delete(assetId, filename);
|
|
94
|
+
if (!result.ok && result.error.code !== "NOT_FOUND") {
|
|
95
|
+
throw new Error(`Failed to delete ${filename}: ${result.error.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
export function registerAudioMixJob(engine) {
|
|
99
|
+
registerJobHandler("audio_mix", async (payload, ctx) => {
|
|
100
|
+
const { targetPath, targetFilename, targetKind, layerPath, layerAssetId, volume, assetId, assetDir, bookId, } = payload;
|
|
101
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "replace_audio", { durationMs: LOCK_DURATION_MS, data: { state: "mixing-audio" } }, async () => {
|
|
102
|
+
const tempDir = await mkdtemp(join(tmpdir(), "videocity-audio-mix-"));
|
|
103
|
+
const extension = extname(targetFilename) || (targetKind === "video" ? ".mp4" : ".mp3");
|
|
104
|
+
const outputPath = join(tempDir, `mixed${extension}`);
|
|
105
|
+
try {
|
|
106
|
+
const outputProbe = await mixAudioMedia(targetPath, layerPath, outputPath, targetKind, volume, ctx.signal);
|
|
107
|
+
await replaceArtifactFileFromPath(engine, assetId, targetFilename, outputPath);
|
|
108
|
+
if (targetKind === "audio") {
|
|
109
|
+
await registerMediaStreams(engine, assetId, targetFilename, outputProbe);
|
|
110
|
+
const previous = await engine.metadata.artifacts.read(assetId, "original");
|
|
111
|
+
const metadata = await engine.metadata.artifacts.write(assetId, "original", {
|
|
112
|
+
...(previous.ok ? previous.value : {}),
|
|
113
|
+
origin: "audio_mix",
|
|
114
|
+
source_audio_asset: layerAssetId,
|
|
115
|
+
layer_volume: volume,
|
|
116
|
+
duration: outputProbe.audioDuration ?? outputProbe.duration,
|
|
117
|
+
});
|
|
118
|
+
if (!metadata.ok)
|
|
119
|
+
throw new Error(metadata.error.message);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
finally {
|
|
123
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
for (const filename of STALE_DERIVATIVES) {
|
|
126
|
+
await deleteIfPresent(engine, assetId, filename);
|
|
127
|
+
}
|
|
128
|
+
const mediaPath = join(assetDir, targetFilename);
|
|
129
|
+
if (targetKind === "audio") {
|
|
130
|
+
const peaks = await extractWaveform(mediaPath);
|
|
131
|
+
const waveformResult = await engine.metadata.waveforms.write(assetId, peaks);
|
|
132
|
+
if (!waveformResult.ok) {
|
|
133
|
+
throw new Error(`Failed to update audio waveform: ${waveformResult.error.message}`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return { mediaPath };
|
|
137
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
138
|
+
if (targetKind === "video") {
|
|
139
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
140
|
+
type: "process_video",
|
|
141
|
+
videoPath: result.mediaPath,
|
|
142
|
+
assetId,
|
|
143
|
+
assetDir,
|
|
144
|
+
bookId,
|
|
145
|
+
provenance: {
|
|
146
|
+
origin: "audio_mix",
|
|
147
|
+
source_audio_asset: layerAssetId,
|
|
148
|
+
layer_volume: volume,
|
|
149
|
+
mixed_at: new Date().toISOString(),
|
|
150
|
+
},
|
|
151
|
+
force: true,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
success: true,
|
|
156
|
+
target_asset_id: assetId,
|
|
157
|
+
layer_asset_id: layerAssetId,
|
|
158
|
+
volume,
|
|
159
|
+
};
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tagging a library that already exists.
|
|
3
|
+
*
|
|
4
|
+
* The pass walks the catalog in bounded pages and runs the same coordinator
|
|
5
|
+
* new assets use, so a backfilled tag and a freshly tagged one are produced by
|
|
6
|
+
* one code path and carry the same fence and provenance.
|
|
7
|
+
*
|
|
8
|
+
* Everything here is shaped by the pass being interruptible. It checks the
|
|
9
|
+
* persisted status between pages, so pause and cancel land within one page
|
|
10
|
+
* rather than at the end of a library; it writes its cursor and counts after
|
|
11
|
+
* every page, so a restart resumes instead of re-running; and it holds no
|
|
12
|
+
* in-memory state that a crash would lose. Adopting cached analysis costs
|
|
13
|
+
* nothing, so the default pass reads caches only and reports how many assets
|
|
14
|
+
* would still need inference — the spend is a separate, explicit choice.
|
|
15
|
+
*/
|
|
16
|
+
import { v7 as uuidv7 } from "uuid";
|
|
17
|
+
import { logger } from "../logger.js";
|
|
18
|
+
import { registerJobHandler } from "../queue.js";
|
|
19
|
+
import { readBackfillState, recordOutcome, writeBackfillState, EMPTY_COUNTS, } from "../tag-backfill-state.js";
|
|
20
|
+
import { runAssetTagging } from "./tag-asset.js";
|
|
21
|
+
/** Assets per page. Small enough that pause lands promptly, large enough that
|
|
22
|
+
* a big library is not dominated by state writes. */
|
|
23
|
+
const PAGE_SIZE = 25;
|
|
24
|
+
/** Assets tagged at once within a page. Tagging is mostly file reads; the
|
|
25
|
+
* provider work it may schedule is bounded by the queue runner, not here. */
|
|
26
|
+
const PAGE_CONCURRENCY = 4;
|
|
27
|
+
async function mapWithLimit(items, limit, run, canRun) {
|
|
28
|
+
const results = [];
|
|
29
|
+
let next = 0;
|
|
30
|
+
async function worker() {
|
|
31
|
+
while (canRun()) {
|
|
32
|
+
const index = next;
|
|
33
|
+
next += 1;
|
|
34
|
+
const item = items[index];
|
|
35
|
+
if (item === undefined)
|
|
36
|
+
return;
|
|
37
|
+
results[index] = await run(item);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
await Promise.all(Array.from({ length: Math.min(limit, items.length) }, () => worker()));
|
|
41
|
+
return results;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Ids after the cursor, in catalog order. The cursor is an artifact id rather
|
|
45
|
+
* than an offset because the catalog changes while the pass runs: deleting one
|
|
46
|
+
* asset behind an offset shifts every later page, silently skipping rows.
|
|
47
|
+
*/
|
|
48
|
+
function pageAfter(engine, cursor) {
|
|
49
|
+
const ids = engine.artifacts
|
|
50
|
+
.list({ sort: "oldest" })
|
|
51
|
+
.map((artifact) => artifact.artifactId);
|
|
52
|
+
const start = cursor ? ids.findIndex((id) => id > cursor) : 0;
|
|
53
|
+
if (start < 0)
|
|
54
|
+
return [];
|
|
55
|
+
return ids.slice(start, start + PAGE_SIZE);
|
|
56
|
+
}
|
|
57
|
+
function totalArtifacts(engine) {
|
|
58
|
+
return engine.artifacts.list().length;
|
|
59
|
+
}
|
|
60
|
+
export async function runAssetTagBackfill(engine, payload, context) {
|
|
61
|
+
let state = readBackfillState(engine);
|
|
62
|
+
const runId = payload.runId ?? null;
|
|
63
|
+
if (state.status !== "running" || state.runId !== runId) {
|
|
64
|
+
return state;
|
|
65
|
+
}
|
|
66
|
+
const canRun = () => {
|
|
67
|
+
const latest = readBackfillState(engine);
|
|
68
|
+
return latest.runId === runId && latest.status === "running" && !context?.signal.aborted;
|
|
69
|
+
};
|
|
70
|
+
for (;;) {
|
|
71
|
+
state = readBackfillState(engine);
|
|
72
|
+
if (state.runId !== runId || state.status !== "running")
|
|
73
|
+
return state;
|
|
74
|
+
if (context?.signal.aborted) {
|
|
75
|
+
return writeBackfillState(engine, { ...state, status: "paused" });
|
|
76
|
+
}
|
|
77
|
+
const page = pageAfter(engine, state.cursor);
|
|
78
|
+
if (page.length === 0) {
|
|
79
|
+
return writeBackfillState(engine, {
|
|
80
|
+
...state,
|
|
81
|
+
status: "completed",
|
|
82
|
+
finishedAt: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const outcomes = await mapWithLimit(page, PAGE_CONCURRENCY, async (assetId) => {
|
|
86
|
+
try {
|
|
87
|
+
const result = await runAssetTagging(engine, {
|
|
88
|
+
type: "tag_asset",
|
|
89
|
+
bookId: payload.bookId,
|
|
90
|
+
assetId,
|
|
91
|
+
...(state.force ? { force: true } : {}),
|
|
92
|
+
...(state.allowAnalysis ? {} : { cacheOnly: true }),
|
|
93
|
+
}, { canRequestAnalysis: canRun });
|
|
94
|
+
return result.outcome;
|
|
95
|
+
}
|
|
96
|
+
catch (cause) {
|
|
97
|
+
logger.warn("tag-asset-failed", { assetId, error: cause });
|
|
98
|
+
return "failed";
|
|
99
|
+
}
|
|
100
|
+
}, canRun);
|
|
101
|
+
const latest = readBackfillState(engine);
|
|
102
|
+
if (latest.runId !== runId)
|
|
103
|
+
return latest;
|
|
104
|
+
let counts = latest.counts;
|
|
105
|
+
for (const outcome of outcomes)
|
|
106
|
+
counts = recordOutcome(counts, outcome);
|
|
107
|
+
state = writeBackfillState(engine, {
|
|
108
|
+
...latest,
|
|
109
|
+
cursor: page[outcomes.length - 1] ?? latest.cursor,
|
|
110
|
+
scanned: latest.scanned + outcomes.length,
|
|
111
|
+
counts,
|
|
112
|
+
});
|
|
113
|
+
if (state.status !== "running")
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/** Starting state for a fresh pass; resume keeps the cursor and counts. */
|
|
118
|
+
export function startedState(previous, options) {
|
|
119
|
+
const resuming = options.resume && previous.status === "paused";
|
|
120
|
+
return {
|
|
121
|
+
runId: uuidv7(),
|
|
122
|
+
status: "running",
|
|
123
|
+
cursor: resuming ? previous.cursor : null,
|
|
124
|
+
scanned: resuming ? previous.scanned : 0,
|
|
125
|
+
counts: resuming ? previous.counts : EMPTY_COUNTS,
|
|
126
|
+
allowAnalysis: options.allowAnalysis,
|
|
127
|
+
force: options.force,
|
|
128
|
+
startedAt: resuming ? previous.startedAt ?? Date.now() : Date.now(),
|
|
129
|
+
updatedAt: Date.now(),
|
|
130
|
+
finishedAt: null,
|
|
131
|
+
lastError: null,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function backfillProgress(engine, state) {
|
|
135
|
+
const total = totalArtifacts(engine);
|
|
136
|
+
return {
|
|
137
|
+
state,
|
|
138
|
+
total,
|
|
139
|
+
remaining: Math.max(0, total - state.scanned),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
export function registerBackfillAssetTagsJob(engine) {
|
|
143
|
+
registerJobHandler("backfill_asset_tags", async (payload, ctx) => await runAssetTagBackfill(engine, payload, ctx));
|
|
144
|
+
}
|