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,149 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { registerJobHandler } from "../queue.js";
|
|
4
|
+
import { getApiKey } from "../api-key.js";
|
|
5
|
+
import { INTELLIGENCE_MODEL, buildIntelligenceResponses, firstResponseText, intelligenceRequest, } from "../openai-intelligence.js";
|
|
6
|
+
import { withLocalLease } from "./_lease.js";
|
|
7
|
+
import { TRIM_BUDGET_FRACTION, DEAD_AIR_THRESHOLD_S, TrimSchema, buildCutsContext, extractSilenceCandidates, filterValidTrims, loadAssetForAnalysis, sampleFrames, } from "../tools/_suggest-shared.js";
|
|
8
|
+
const SYSTEM_PROMPT_CUTS = `You are a senior video editor reviewing a short clip. From the timestamped frames, optional transcript, and metadata you receive, return a strict JSON object suggesting sections to cut. Follow the user's editing request when provided.
|
|
9
|
+
|
|
10
|
+
TRIM RULES (read carefully — most failures come from breaking these):
|
|
11
|
+
|
|
12
|
+
1. Dead-air trims (severity "high"):
|
|
13
|
+
- MUST be drawn from the "Silence candidates" list provided in the user message.
|
|
14
|
+
- You MAY shrink a candidate to be more conservative (e.g., keep 0.2-0.4s of natural pause on each side). You MUST NOT expand a candidate or invent one not in the list.
|
|
15
|
+
- You MAY merge two adjacent candidates only if they are < 0.5s apart.
|
|
16
|
+
- If the user message says "no silence candidates", do not suggest any "high" severity trims.
|
|
17
|
+
|
|
18
|
+
2. Filler-cluster trims (severity "med"):
|
|
19
|
+
- Require a word-timed transcript. Without word timings, do not suggest any "med" trims.
|
|
20
|
+
- The transcript MUST contain the filler verbatim ("um", "uh", "like", "you know") at the proposed timestamp range.
|
|
21
|
+
- Range MUST be tight around the filler word(s), typically < 1.5s.
|
|
22
|
+
- Do not propose a "med" trim spanning meaningful content.
|
|
23
|
+
|
|
24
|
+
3. Visual pacing / minor trims (severity "low"):
|
|
25
|
+
- Use timestamped frames to identify clearly redundant visual holds or awkward lead-ins and tails. Explain the visible evidence and keep the ranges conservative.
|
|
26
|
+
- Without a transcript, only these visually supported "low" trims are allowed. Missing transcription is not evidence of silence or absence of dialogue. State in the summary that dialogue and audio timing were not assessed.
|
|
27
|
+
- Sparse frames do not prove what happens between them. Do not invent precise action boundaries; omit uncertain cuts.
|
|
28
|
+
|
|
29
|
+
4. Hard limits:
|
|
30
|
+
- Total trim duration (sum of all cut_end - cut_start) MUST be <= the budget given in the user message.
|
|
31
|
+
- Never overlap trim ranges. cut_start < cut_end. Both within [0, duration].
|
|
32
|
+
- Do not cut spoken dialogue that completes a sentence. Do not cut audible chants, laughter, or other deliberate audio events.
|
|
33
|
+
- If unsure, omit. Empty trims is a valid answer.
|
|
34
|
+
|
|
35
|
+
OUTPUT:
|
|
36
|
+
- Decimal seconds (e.g. 12.4).
|
|
37
|
+
- Do not invent content. Use what is visible in the frames or quoted in the transcript.
|
|
38
|
+
- Prefer fewer high-confidence suggestions over noisy lists.
|
|
39
|
+
- Output JSON only.`;
|
|
40
|
+
const SUGGEST_CUTS_JSON_SCHEMA = {
|
|
41
|
+
type: "object",
|
|
42
|
+
required: ["trims", "summary"],
|
|
43
|
+
additionalProperties: false,
|
|
44
|
+
properties: {
|
|
45
|
+
trims: {
|
|
46
|
+
type: "array",
|
|
47
|
+
items: {
|
|
48
|
+
type: "object",
|
|
49
|
+
required: ["cut_start", "cut_end", "reason", "severity"],
|
|
50
|
+
additionalProperties: false,
|
|
51
|
+
properties: {
|
|
52
|
+
cut_start: { type: "number" },
|
|
53
|
+
cut_end: { type: "number" },
|
|
54
|
+
reason: { type: "string" },
|
|
55
|
+
severity: { type: "string", enum: ["low", "med", "high"] },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
summary: { type: "string" },
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
const ModelOutputSchema = z.object({
|
|
63
|
+
trims: z.array(TrimSchema),
|
|
64
|
+
summary: z.string(),
|
|
65
|
+
});
|
|
66
|
+
export function registerSuggestCutsJob(engine) {
|
|
67
|
+
registerJobHandler("suggest_cuts", async (payload, ctx) => {
|
|
68
|
+
const { assetId, instructions } = payload;
|
|
69
|
+
return withLocalLease(engine, assetId, ctx, "analyze", async () => {
|
|
70
|
+
const apiKey = getApiKey("OPENAI_API_KEY");
|
|
71
|
+
if (!apiKey)
|
|
72
|
+
throw new Error("OPENAI_API_KEY environment variable is not set");
|
|
73
|
+
const loaded = await loadAssetForAnalysis(engine, assetId, { requireTranscript: false });
|
|
74
|
+
if (!loaded.ok)
|
|
75
|
+
throw new Error(loaded.error.message);
|
|
76
|
+
const { frameNames, transcript, meta, prior } = loaded.value;
|
|
77
|
+
const candidates = transcript.source === "elevenlabs"
|
|
78
|
+
? extractSilenceCandidates(transcript.words, DEAD_AIR_THRESHOLD_S)
|
|
79
|
+
: [];
|
|
80
|
+
const duration = typeof meta?.duration === "number" && Number.isFinite(meta.duration) && meta.duration > 0
|
|
81
|
+
? meta.duration : null;
|
|
82
|
+
const trimBudgetS = duration !== null ? duration * TRIM_BUDGET_FRACTION : Infinity;
|
|
83
|
+
const sampled = sampleFrames(frameNames);
|
|
84
|
+
const frameParts = [];
|
|
85
|
+
for (const file of sampled) {
|
|
86
|
+
const frameRead = await engine.files.read(assetId, `original_frames/${file}`);
|
|
87
|
+
if (!frameRead.ok)
|
|
88
|
+
throw new Error(frameRead.error.message);
|
|
89
|
+
frameParts.push({ type: "input_text", text: `Frame timestamp: ${parseFloat(file).toFixed(3)}s` }, {
|
|
90
|
+
type: "input_image",
|
|
91
|
+
image_url: `data:image/jpeg;base64,${Buffer.from(frameRead.value).toString("base64")}`,
|
|
92
|
+
detail: "high",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
const contextString = buildCutsContext(meta, prior, transcript, sampled.length, candidates, trimBudgetS, instructions);
|
|
96
|
+
const userContent = [
|
|
97
|
+
{ type: "input_text", text: contextString },
|
|
98
|
+
...frameParts,
|
|
99
|
+
];
|
|
100
|
+
const responses = buildIntelligenceResponses(apiKey);
|
|
101
|
+
const response = await responses(intelligenceRequest({
|
|
102
|
+
instructions: SYSTEM_PROMPT_CUTS,
|
|
103
|
+
input: [{ role: "user", content: userContent }],
|
|
104
|
+
text: {
|
|
105
|
+
format: {
|
|
106
|
+
type: "json_schema",
|
|
107
|
+
name: "suggested_cuts",
|
|
108
|
+
schema: SUGGEST_CUTS_JSON_SCHEMA,
|
|
109
|
+
strict: true,
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
max_output_tokens: 4096,
|
|
113
|
+
}), ctx.signal);
|
|
114
|
+
const outputText = firstResponseText(response);
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = ModelOutputSchema.parse(JSON.parse(outputText));
|
|
118
|
+
}
|
|
119
|
+
catch (err) {
|
|
120
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
121
|
+
throw new Error(`Model output failed schema validation: ${msg}`);
|
|
122
|
+
}
|
|
123
|
+
const { kept, dropped } = filterValidTrims(parsed.trims, candidates, trimBudgetS, {
|
|
124
|
+
duration, transcriptSource: transcript.source,
|
|
125
|
+
});
|
|
126
|
+
if (dropped.length > 0) {
|
|
127
|
+
logger.warn("suggest-cuts-dropped", { assetId, count: dropped.length });
|
|
128
|
+
}
|
|
129
|
+
const final = {
|
|
130
|
+
trims: kept,
|
|
131
|
+
summary: parsed.summary,
|
|
132
|
+
based_on: {
|
|
133
|
+
frames: sampled.length,
|
|
134
|
+
transcript_source: transcript.source,
|
|
135
|
+
silence_candidates: candidates,
|
|
136
|
+
trim_budget_s: Number.isFinite(trimBudgetS) ? trimBudgetS : null,
|
|
137
|
+
dropped_trims: dropped.map((d) => ({ trim: d.trim, reason: d.reason })),
|
|
138
|
+
},
|
|
139
|
+
model: INTELLIGENCE_MODEL,
|
|
140
|
+
asset_id: assetId,
|
|
141
|
+
};
|
|
142
|
+
const writeResult = await engine.files.write(assetId, ".suggest-cuts.json", JSON.stringify(final, null, 2));
|
|
143
|
+
if (!writeResult.ok) {
|
|
144
|
+
throw new Error(`Suggestions generated but failed to persist: ${writeResult.error.message}`);
|
|
145
|
+
}
|
|
146
|
+
return { success: true, trims: kept.length, dropped: dropped.length };
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { logger } from "../logger.js";
|
|
3
|
+
import { registerJobHandler } from "../queue.js";
|
|
4
|
+
import { getApiKey } from "../api-key.js";
|
|
5
|
+
import { INTELLIGENCE_MODEL, buildIntelligenceResponses, firstResponseText, intelligenceRequest, } from "../openai-intelligence.js";
|
|
6
|
+
import { withLocalLease } from "./_lease.js";
|
|
7
|
+
import { SfxSchema, buildSfxContext, loadAssetForAnalysis, sampleFrames, } from "../tools/_suggest-shared.js";
|
|
8
|
+
const MAX_SFX = 8;
|
|
9
|
+
const TIMESTAMP_END_BUFFER_S = 0.05;
|
|
10
|
+
const SYSTEM_PROMPT_SFX = `You are a senior video editor reviewing a short clip. From the frames, transcript, and metadata you receive, return a strict JSON object suggesting sound effects to add.
|
|
11
|
+
|
|
12
|
+
SFX RULES:
|
|
13
|
+
- type: short slug ("whoosh", "impact", "pop", "ding", "ambient_room", "transition_swell").
|
|
14
|
+
- Suggest only at clear visual or narrative beats (cuts, reveals, punchlines, scene changes). Cap at ${MAX_SFX}.
|
|
15
|
+
- description: one sentence on what is on screen and why this SFX fits.
|
|
16
|
+
|
|
17
|
+
OUTPUT:
|
|
18
|
+
- Decimal seconds (e.g. 12.4).
|
|
19
|
+
- Do not invent content. Use what is visible in the frames or quoted in the transcript.
|
|
20
|
+
- Prefer fewer high-confidence suggestions over noisy lists.
|
|
21
|
+
- Output JSON only, max ${MAX_SFX} entries.`;
|
|
22
|
+
const SUGGEST_SFX_JSON_SCHEMA = {
|
|
23
|
+
type: "object",
|
|
24
|
+
required: ["sfx", "summary"],
|
|
25
|
+
additionalProperties: false,
|
|
26
|
+
properties: {
|
|
27
|
+
sfx: {
|
|
28
|
+
type: "array",
|
|
29
|
+
items: {
|
|
30
|
+
type: "object",
|
|
31
|
+
required: ["timestamp", "type", "description"],
|
|
32
|
+
additionalProperties: false,
|
|
33
|
+
properties: {
|
|
34
|
+
timestamp: { type: "number" },
|
|
35
|
+
type: { type: "string" },
|
|
36
|
+
description: { type: "string" },
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
summary: { type: "string" },
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
const ModelOutputSchema = z.object({
|
|
44
|
+
sfx: z.array(SfxSchema),
|
|
45
|
+
summary: z.string(),
|
|
46
|
+
});
|
|
47
|
+
export function registerSuggestSfxJob(engine) {
|
|
48
|
+
registerJobHandler("suggest_sfx", async (payload, ctx) => {
|
|
49
|
+
const { assetId } = payload;
|
|
50
|
+
return withLocalLease(engine, assetId, ctx, "analyze", async () => {
|
|
51
|
+
const apiKey = getApiKey("OPENAI_API_KEY");
|
|
52
|
+
if (!apiKey)
|
|
53
|
+
throw new Error("OPENAI_API_KEY environment variable is not set");
|
|
54
|
+
const loaded = await loadAssetForAnalysis(engine, assetId);
|
|
55
|
+
if (!loaded.ok)
|
|
56
|
+
throw new Error(loaded.error.message);
|
|
57
|
+
const { frameNames, transcript, meta, prior } = loaded.value;
|
|
58
|
+
const sampled = sampleFrames(frameNames);
|
|
59
|
+
const frameParts = [];
|
|
60
|
+
for (const file of sampled) {
|
|
61
|
+
const frameRead = await engine.files.read(assetId, `original_frames/${file}`);
|
|
62
|
+
if (!frameRead.ok)
|
|
63
|
+
throw new Error(frameRead.error.message);
|
|
64
|
+
frameParts.push({ type: "input_text", text: `Frame timestamp: ${parseFloat(file).toFixed(3)}s` }, {
|
|
65
|
+
type: "input_image",
|
|
66
|
+
image_url: `data:image/jpeg;base64,${Buffer.from(frameRead.value).toString("base64")}`,
|
|
67
|
+
detail: "high",
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
const contextString = buildSfxContext(meta, prior, transcript, sampled.length);
|
|
71
|
+
const userContent = [
|
|
72
|
+
{ type: "input_text", text: contextString },
|
|
73
|
+
...frameParts,
|
|
74
|
+
];
|
|
75
|
+
const responses = buildIntelligenceResponses(apiKey);
|
|
76
|
+
const response = await responses(intelligenceRequest({
|
|
77
|
+
instructions: SYSTEM_PROMPT_SFX,
|
|
78
|
+
input: [{ role: "user", content: userContent }],
|
|
79
|
+
text: {
|
|
80
|
+
format: {
|
|
81
|
+
type: "json_schema",
|
|
82
|
+
name: "suggested_sound_effects",
|
|
83
|
+
schema: SUGGEST_SFX_JSON_SCHEMA,
|
|
84
|
+
strict: true,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
max_output_tokens: 4096,
|
|
88
|
+
}), ctx.signal);
|
|
89
|
+
const outputText = firstResponseText(response);
|
|
90
|
+
let parsed;
|
|
91
|
+
try {
|
|
92
|
+
parsed = ModelOutputSchema.parse(JSON.parse(outputText));
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
96
|
+
throw new Error(`Model output failed schema validation: ${msg}`);
|
|
97
|
+
}
|
|
98
|
+
const duration = typeof meta?.duration === "number" ? meta.duration : null;
|
|
99
|
+
const droppedSfx = [];
|
|
100
|
+
const validSfx = [];
|
|
101
|
+
for (const entry of parsed.sfx) {
|
|
102
|
+
if (duration !== null && entry.timestamp > duration - TIMESTAMP_END_BUFFER_S) {
|
|
103
|
+
droppedSfx.push({ sfx: entry, reason: `timestamp ${entry.timestamp.toFixed(2)}s past duration ${duration.toFixed(2)}s` });
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
validSfx.push(entry);
|
|
107
|
+
}
|
|
108
|
+
const capped = validSfx.slice(0, MAX_SFX);
|
|
109
|
+
if (validSfx.length > MAX_SFX) {
|
|
110
|
+
for (const entry of validSfx.slice(MAX_SFX)) {
|
|
111
|
+
droppedSfx.push({ sfx: entry, reason: `cap of ${MAX_SFX} entries exceeded` });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (droppedSfx.length > 0) {
|
|
115
|
+
logger.warn("suggest-sfx-dropped", { assetId, count: droppedSfx.length });
|
|
116
|
+
}
|
|
117
|
+
const final = {
|
|
118
|
+
sfx: capped,
|
|
119
|
+
summary: parsed.summary,
|
|
120
|
+
based_on: {
|
|
121
|
+
frames: sampled.length,
|
|
122
|
+
transcript_source: transcript.source,
|
|
123
|
+
dropped_sfx: droppedSfx,
|
|
124
|
+
},
|
|
125
|
+
model: INTELLIGENCE_MODEL,
|
|
126
|
+
asset_id: assetId,
|
|
127
|
+
};
|
|
128
|
+
const writeResult = await engine.files.write(assetId, ".suggest-sfx.json", JSON.stringify(final, null, 2));
|
|
129
|
+
if (!writeResult.ok) {
|
|
130
|
+
throw new Error(`Suggestions generated but failed to persist: ${writeResult.error.message}`);
|
|
131
|
+
}
|
|
132
|
+
return { success: true, sfx: capped.length, dropped: droppedSfx.length };
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { findAudioFile, findVideoFile } from "videobook-engine";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { basename, join } from "node:path";
|
|
4
|
+
import { hasTimedSpeechWords, resolveAudioCue } from "../audio-cue.js";
|
|
5
|
+
import { buildAudioSyncVideoFilter, createAudioSyncPlan, } from "../audio-sync.js";
|
|
6
|
+
import { AUDIO_OPTS, H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
|
|
7
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
8
|
+
import { requestElevenLabsTranscript, writeElevenLabsTranscript, } from "./_transcribe.js";
|
|
9
|
+
import { claimLockedLease, withLockedLocalLease } from "./_lease.js";
|
|
10
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
11
|
+
const LOCK_DURATION_MS = 900_000;
|
|
12
|
+
const AUTO_TRANSCRIBE_OPTIONS = {
|
|
13
|
+
tagAudioEvents: true,
|
|
14
|
+
diarize: true,
|
|
15
|
+
};
|
|
16
|
+
const STALE_VISUAL_DERIVATIVES = [
|
|
17
|
+
".original.analysis.json",
|
|
18
|
+
".detect-scenes.json",
|
|
19
|
+
".suggest-cuts.json",
|
|
20
|
+
".suggest-sfx.json",
|
|
21
|
+
".analyze-jokes.json",
|
|
22
|
+
];
|
|
23
|
+
export function resolveSyncVideoToAudioAnchors(anchors, transcript) {
|
|
24
|
+
return anchors.map((anchor) => ({
|
|
25
|
+
videoSeconds: anchor.videoSeconds,
|
|
26
|
+
audioSeconds: "audioSeconds" in anchor
|
|
27
|
+
? anchor.audioSeconds
|
|
28
|
+
: resolveAudioCue(transcript, anchor.audioCue),
|
|
29
|
+
}));
|
|
30
|
+
}
|
|
31
|
+
function outputArgs(inputPath, outputPath, plan, audioMode) {
|
|
32
|
+
return [
|
|
33
|
+
"-y",
|
|
34
|
+
"-i", inputPath,
|
|
35
|
+
"-filter_complex", buildAudioSyncVideoFilter(plan),
|
|
36
|
+
"-map", "[vout]",
|
|
37
|
+
"-map", "0:a:0",
|
|
38
|
+
...H264_MEDIUM,
|
|
39
|
+
"-pix_fmt", "yuv420p",
|
|
40
|
+
...(audioMode === "copy" ? ["-c:a", "copy"] : AUDIO_OPTS),
|
|
41
|
+
"-t", plan.audioDuration.toFixed(9),
|
|
42
|
+
"-movflags", "+faststart",
|
|
43
|
+
"-f", "mp4",
|
|
44
|
+
outputPath,
|
|
45
|
+
];
|
|
46
|
+
}
|
|
47
|
+
export async function renderAudioSyncedVideo(inputPath, outputPath, plan, signal) {
|
|
48
|
+
const source = await probeVideo(inputPath, { signal });
|
|
49
|
+
if (source.audioCodec !== "aac") {
|
|
50
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, "aac"), { signal });
|
|
51
|
+
return "aac";
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, "copy"), { signal });
|
|
55
|
+
return "copy";
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, "aac"), { signal });
|
|
59
|
+
return "aac";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export async function validateAudioSyncOutput(inputPath, outputPath, plan, signal) {
|
|
63
|
+
const [input, output] = await Promise.all([
|
|
64
|
+
probeVideo(inputPath, { signal }),
|
|
65
|
+
probeVideo(outputPath, { signal }),
|
|
66
|
+
]);
|
|
67
|
+
if (output.videoCodec === null || output.width <= 0 || output.height <= 0) {
|
|
68
|
+
throw new Error("Audio-sync output has no usable video stream");
|
|
69
|
+
}
|
|
70
|
+
if (!output.hasAudio || output.audioDuration === null) {
|
|
71
|
+
throw new Error("Audio-sync output does not contain the original audio stream");
|
|
72
|
+
}
|
|
73
|
+
if (output.width !== input.width || output.height !== input.height) {
|
|
74
|
+
throw new Error(`Audio-sync output dimensions ${output.width}x${output.height} do not match source ${input.width}x${input.height}`);
|
|
75
|
+
}
|
|
76
|
+
const tolerance = Math.max(0.08, 2 / plan.sourceFps);
|
|
77
|
+
if (Math.abs(output.videoDuration - plan.audioDuration) > tolerance) {
|
|
78
|
+
throw new Error(`Audio-sync video duration ${output.videoDuration.toFixed(3)}s does not match audio ${plan.audioDuration.toFixed(3)}s`);
|
|
79
|
+
}
|
|
80
|
+
if (Math.abs(output.audioDuration - plan.audioDuration) > tolerance) {
|
|
81
|
+
throw new Error(`Audio-sync output audio duration ${output.audioDuration.toFixed(3)}s does not match source audio ${plan.audioDuration.toFixed(3)}s`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function readUsableTimedTranscript(engine, assetId) {
|
|
85
|
+
const result = await engine.files.read(assetId, "elevenlabs.json");
|
|
86
|
+
if (!result.ok) {
|
|
87
|
+
if (result.error.code === "NOT_FOUND")
|
|
88
|
+
return null;
|
|
89
|
+
throw new Error(`Failed to read elevenlabs.json: ${result.error.message}`);
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
const transcript = JSON.parse(result.value.toString("utf8"));
|
|
93
|
+
return hasTimedSpeechWords(transcript) ? transcript : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export async function prepareAudioSyncTranscript(engine, assetId) {
|
|
100
|
+
const existing = await readUsableTimedTranscript(engine, assetId);
|
|
101
|
+
if (existing !== null)
|
|
102
|
+
return { transcript: existing, replacement: null };
|
|
103
|
+
const manifest = await engine.files.manifest(assetId);
|
|
104
|
+
if (!manifest.ok)
|
|
105
|
+
throw new Error(`auto-transcribe failed: ${manifest.error.message}`);
|
|
106
|
+
const artifact = engine.artifacts.get(assetId);
|
|
107
|
+
if (!artifact.ok)
|
|
108
|
+
throw new Error(`auto-transcribe failed: ${artifact.error.message}`);
|
|
109
|
+
const audioFile = findAudioFile(manifest.value.files, artifact.value.kind)
|
|
110
|
+
?? findVideoFile(manifest.value.files);
|
|
111
|
+
if (!audioFile)
|
|
112
|
+
throw new Error(`auto-transcribe failed: no audio file found in asset ${assetId}`);
|
|
113
|
+
const replacement = await requestElevenLabsTranscript(engine, assetId, audioFile.name, AUTO_TRANSCRIBE_OPTIONS);
|
|
114
|
+
if (!hasTimedSpeechWords(replacement)) {
|
|
115
|
+
throw new Error(`Auto-transcription for ${assetId} did not return timed speech words`);
|
|
116
|
+
}
|
|
117
|
+
return { transcript: replacement, replacement };
|
|
118
|
+
}
|
|
119
|
+
async function deleteIfPresent(engine, assetId, filename) {
|
|
120
|
+
const result = await engine.files.delete(assetId, filename);
|
|
121
|
+
if (!result.ok && result.error.code !== "NOT_FOUND") {
|
|
122
|
+
throw new Error(`Failed to delete ${filename}: ${result.error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async function discardTemp(outputPath) {
|
|
126
|
+
await rm(outputPath, { force: true });
|
|
127
|
+
}
|
|
128
|
+
function errorMessage(error) {
|
|
129
|
+
return error instanceof Error ? error.message : String(error);
|
|
130
|
+
}
|
|
131
|
+
function preserveArtifactState(error) {
|
|
132
|
+
const preserved = error instanceof Error ? error : new Error(String(error));
|
|
133
|
+
Object.assign(preserved, { preserveArtifactState: true });
|
|
134
|
+
return preserved;
|
|
135
|
+
}
|
|
136
|
+
export async function restoreAudioSyncSnapshot(engine, assetId, restoreCommit) {
|
|
137
|
+
const artifact = engine.artifacts.get(assetId);
|
|
138
|
+
if (!artifact.ok) {
|
|
139
|
+
throw new Error(`Failed to resolve ${assetId} for restore: ${artifact.error.message}`);
|
|
140
|
+
}
|
|
141
|
+
const restored = await engine.history.restoreArtifact(artifact.value.artifactId, restoreCommit);
|
|
142
|
+
if (!restored.ok) {
|
|
143
|
+
throw new Error(`Failed to restore ${assetId} to ${restoreCommit}: ${restored.error.message}`);
|
|
144
|
+
}
|
|
145
|
+
return restored.value.hash;
|
|
146
|
+
}
|
|
147
|
+
function rollbackError(original, failures) {
|
|
148
|
+
return new Error(`Audio-sync failed: ${errorMessage(original)}. Rollback failed: ${failures.map(errorMessage).join("; ")}`);
|
|
149
|
+
}
|
|
150
|
+
async function clearAudioSyncFailure(engine, assetId) {
|
|
151
|
+
const cleared = await engine.jobs.failures.clear(assetId);
|
|
152
|
+
if (!cleared.ok) {
|
|
153
|
+
throw new Error(`Failed to clear audio-sync error for ${assetId}: ${cleared.error.message}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async function rollbackAudioSyncFailure(engine, assetId, outputPath, restoreCommit, original, markFailureHandled) {
|
|
157
|
+
const failures = [];
|
|
158
|
+
try {
|
|
159
|
+
await discardTemp(outputPath);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
failures.push(error);
|
|
163
|
+
}
|
|
164
|
+
try {
|
|
165
|
+
await clearAudioSyncFailure(engine, assetId);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
failures.push(error);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
await restoreAudioSyncSnapshot(engine, assetId, restoreCommit);
|
|
172
|
+
}
|
|
173
|
+
catch (error) {
|
|
174
|
+
failures.push(error);
|
|
175
|
+
}
|
|
176
|
+
if (failures.length > 0)
|
|
177
|
+
throw rollbackError(original, failures);
|
|
178
|
+
markFailureHandled();
|
|
179
|
+
throw preserveArtifactState(original);
|
|
180
|
+
}
|
|
181
|
+
async function restoreAfterEnqueueFailure(engine, assetId, assetDir, restoreCommit) {
|
|
182
|
+
const claim = await claimLockedLease(engine, assetId, assetDir, "change_speed", { durationMs: LOCK_DURATION_MS, data: { state: "rolling_back_audio_sync" } }, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
183
|
+
if (!claim.ok)
|
|
184
|
+
throw new Error(`Could not claim ${assetId} for rollback: ${claim.reason}`);
|
|
185
|
+
const released = await engine.jobs.locks.release(assetDir);
|
|
186
|
+
if (!released.ok) {
|
|
187
|
+
await engine.jobs.artifactWork.fail(assetId, claim.ownerId, {
|
|
188
|
+
message: released.error.message,
|
|
189
|
+
}).catch(() => undefined);
|
|
190
|
+
throw new Error(`Failed to release ${assetId} lock before rollback: ${released.error.message}`);
|
|
191
|
+
}
|
|
192
|
+
let restored = false;
|
|
193
|
+
try {
|
|
194
|
+
await clearAudioSyncFailure(engine, assetId);
|
|
195
|
+
await restoreAudioSyncSnapshot(engine, assetId, restoreCommit);
|
|
196
|
+
restored = true;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
await engine.jobs.artifactWork.fail(assetId, claim.ownerId, {
|
|
200
|
+
message: errorMessage(error),
|
|
201
|
+
}).catch(() => undefined);
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
finally {
|
|
205
|
+
if (restored) {
|
|
206
|
+
engine.jobs.artifactWork.complete(assetId, claim.ownerId);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function requestedAnchorMetadata(anchor) {
|
|
211
|
+
return "audioSeconds" in anchor
|
|
212
|
+
? { video_seconds: anchor.videoSeconds, audio_seconds: anchor.audioSeconds }
|
|
213
|
+
: { video_seconds: anchor.videoSeconds, audio_cue: anchor.audioCue };
|
|
214
|
+
}
|
|
215
|
+
export function registerSyncVideoToAudioJob(engine) {
|
|
216
|
+
registerJobHandler("sync_video_to_audio", async (payload, ctx) => {
|
|
217
|
+
const { inputPath, outputPath, anchors, assetId, assetDir, bookId, } = payload;
|
|
218
|
+
const rollbackState = {
|
|
219
|
+
enteredLease: false,
|
|
220
|
+
restoreCommit: null,
|
|
221
|
+
};
|
|
222
|
+
let result;
|
|
223
|
+
try {
|
|
224
|
+
result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "change_speed", { durationMs: LOCK_DURATION_MS, data: { state: "syncing_audio" } }, async () => {
|
|
225
|
+
rollbackState.enteredLease = true;
|
|
226
|
+
const history = engine.history.artifact(assetId);
|
|
227
|
+
rollbackState.restoreCommit = history[0]?.hash ?? null;
|
|
228
|
+
if (!rollbackState.restoreCommit) {
|
|
229
|
+
throw new Error(`sync_video_to_audio requires revision history for ${assetId}`);
|
|
230
|
+
}
|
|
231
|
+
const source = await probeVideo(inputPath, { signal: ctx.signal });
|
|
232
|
+
if (source.videoCodec === null || source.videoDuration <= 0) {
|
|
233
|
+
throw new Error(`sync_video_to_audio requires a usable video stream in ${assetId}`);
|
|
234
|
+
}
|
|
235
|
+
if (!source.hasAudio || source.audioDuration === null || source.audioDuration <= 0) {
|
|
236
|
+
throw new Error(`sync_video_to_audio requires an audio stream in ${assetId}`);
|
|
237
|
+
}
|
|
238
|
+
const preparedTranscript = anchors.some((anchor) => "audioCue" in anchor)
|
|
239
|
+
? await prepareAudioSyncTranscript(engine, assetId)
|
|
240
|
+
: null;
|
|
241
|
+
const resolvedAnchors = resolveSyncVideoToAudioAnchors(anchors, preparedTranscript?.transcript);
|
|
242
|
+
const plan = createAudioSyncPlan({
|
|
243
|
+
videoDuration: source.videoDuration,
|
|
244
|
+
audioDuration: source.audioDuration,
|
|
245
|
+
sourceFps: source.fps,
|
|
246
|
+
anchors: resolvedAnchors,
|
|
247
|
+
});
|
|
248
|
+
const audioMode = await renderAudioSyncedVideo(inputPath, outputPath, plan, ctx.signal);
|
|
249
|
+
await validateAudioSyncOutput(inputPath, outputPath, plan, ctx.signal);
|
|
250
|
+
const inputFilename = basename(inputPath);
|
|
251
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
252
|
+
if (preparedTranscript?.replacement) {
|
|
253
|
+
await writeElevenLabsTranscript(engine, assetId, preparedTranscript.replacement, AUTO_TRANSCRIBE_OPTIONS);
|
|
254
|
+
}
|
|
255
|
+
for (const filename of STALE_VISUAL_DERIVATIVES) {
|
|
256
|
+
await deleteIfPresent(engine, assetId, filename);
|
|
257
|
+
}
|
|
258
|
+
return {
|
|
259
|
+
videoPath: join(assetDir, basename(inputPath)),
|
|
260
|
+
plan,
|
|
261
|
+
requestedAnchors: anchors,
|
|
262
|
+
resolvedAnchors,
|
|
263
|
+
audioMode,
|
|
264
|
+
restoreCommit: rollbackState.restoreCommit,
|
|
265
|
+
};
|
|
266
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
if (!rollbackState.enteredLease)
|
|
270
|
+
throw error;
|
|
271
|
+
if (rollbackState.restoreCommit === null) {
|
|
272
|
+
try {
|
|
273
|
+
await clearAudioSyncFailure(engine, assetId);
|
|
274
|
+
}
|
|
275
|
+
catch (clearError) {
|
|
276
|
+
throw rollbackError(error, [clearError]);
|
|
277
|
+
}
|
|
278
|
+
throw error;
|
|
279
|
+
}
|
|
280
|
+
return rollbackAudioSyncFailure(engine, assetId, outputPath, rollbackState.restoreCommit, error, () => ctx.markAssetFailureHandled());
|
|
281
|
+
}
|
|
282
|
+
try {
|
|
283
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
284
|
+
type: "process_video",
|
|
285
|
+
videoPath: result.videoPath,
|
|
286
|
+
assetId,
|
|
287
|
+
assetDir,
|
|
288
|
+
bookId,
|
|
289
|
+
provenance: {
|
|
290
|
+
origin: "audio_sync",
|
|
291
|
+
requested_anchors: result.requestedAnchors.map(requestedAnchorMetadata),
|
|
292
|
+
resolved_anchors: result.plan.anchors.map((anchor) => ({
|
|
293
|
+
video_seconds: anchor.videoSeconds,
|
|
294
|
+
audio_seconds: anchor.audioSeconds,
|
|
295
|
+
})),
|
|
296
|
+
video_duration: result.plan.videoDuration,
|
|
297
|
+
audio_duration: result.plan.audioDuration,
|
|
298
|
+
mapping_method: "monotone_cubic_hermite",
|
|
299
|
+
audio_preserved: true,
|
|
300
|
+
audio_mode: result.audioMode,
|
|
301
|
+
audio_synced_at: new Date().toISOString(),
|
|
302
|
+
},
|
|
303
|
+
force: true,
|
|
304
|
+
skipDownstreamJobs: true,
|
|
305
|
+
preserveAudioArtifacts: true,
|
|
306
|
+
reviveVisualAnalysis: true,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
try {
|
|
311
|
+
await restoreAfterEnqueueFailure(engine, assetId, assetDir, result.restoreCommit);
|
|
312
|
+
}
|
|
313
|
+
catch (restoreError) {
|
|
314
|
+
throw rollbackError(error, [restoreError]);
|
|
315
|
+
}
|
|
316
|
+
throw preserveArtifactState(error);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
success: true,
|
|
320
|
+
video_duration: result.plan.videoDuration,
|
|
321
|
+
audio_duration: result.plan.audioDuration,
|
|
322
|
+
resolved_anchors: result.resolvedAnchors.map((anchor) => ({
|
|
323
|
+
video_seconds: anchor.videoSeconds,
|
|
324
|
+
audio_seconds: anchor.audioSeconds,
|
|
325
|
+
})),
|
|
326
|
+
audio_mode: result.audioMode,
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}
|