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,316 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { AUDIO_OPTS, H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
6
|
+
import { scoreWatermarkCornersForCanvas, WATERMARK_MIN_DIM_FRACTION, WATERMARK_OPACITY, } from "../remotion/osg-spec.js";
|
|
7
|
+
import { BADGE_WIDTH, DEFAULT_HANDLE_PADDING, HANDLE_ONLY_BADGE_HEIGHT, handlePillHeight, } from "../watermark-badge.js";
|
|
8
|
+
import { DEFAULT_HANDLE_VERTICAL_PERCENT, watermarkHandleOverlayPosition, } from "../watermark-handle-position.js";
|
|
9
|
+
import { analyzeOsgPlacement } from "./osg-placement.js";
|
|
10
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
11
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
12
|
+
import { effectiveWatermarkTiming } from "../watermark-timing.js";
|
|
13
|
+
const LOCK_DURATION_MS = 600_000;
|
|
14
|
+
/** Fade applied at a watermark edge that falls inside the clip. */
|
|
15
|
+
const DEFAULT_WATERMARK_FADE_SECONDS = 0.5;
|
|
16
|
+
export const MAX_WATERMARK_FADE_SECONDS = 2;
|
|
17
|
+
/** Extra looped badge frames past the last fade so overlay never runs dry mid-ramp. */
|
|
18
|
+
const LOOP_MARGIN_SECONDS = 0.25;
|
|
19
|
+
/** Ends this close to the clip end count as "whole clip": no fade out. */
|
|
20
|
+
const CLIP_END_TOLERANCE_SECONDS = 0.001;
|
|
21
|
+
const NO_FADES = { fadeIn: 0, fadeOut: 0 };
|
|
22
|
+
function roundSeconds(value) {
|
|
23
|
+
return Math.round(value * 1000) / 1000;
|
|
24
|
+
}
|
|
25
|
+
/** ffmpeg overlay x/y expressions for a corner, inset by `padding` pixels. */
|
|
26
|
+
function burnOverlayPosition(corner, padding) {
|
|
27
|
+
const [vertical, horizontal] = corner.split("-");
|
|
28
|
+
const x = horizontal === "left"
|
|
29
|
+
? String(padding)
|
|
30
|
+
: horizontal === "right"
|
|
31
|
+
? `main_w-overlay_w-${padding}`
|
|
32
|
+
: "(main_w-overlay_w)/2";
|
|
33
|
+
const y = vertical === "top"
|
|
34
|
+
? String(padding)
|
|
35
|
+
: vertical === "bottom"
|
|
36
|
+
? `main_h-overlay_h-${padding}`
|
|
37
|
+
: "(main_h-overlay_h)/2";
|
|
38
|
+
return { x, y };
|
|
39
|
+
}
|
|
40
|
+
export function burnWatermarkEnable(startSeconds, endSeconds) {
|
|
41
|
+
if (startSeconds === undefined && endSeconds === undefined)
|
|
42
|
+
return "";
|
|
43
|
+
const start = startSeconds ?? 0;
|
|
44
|
+
return endSeconds === undefined
|
|
45
|
+
? `:enable='gte(t,${start})'`
|
|
46
|
+
: `:enable='between(t,${start},${endSeconds})'`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Fade lengths for one watermark window. A watermark fades in only when it
|
|
50
|
+
* starts after the clip start and out only when it ends before the clip end,
|
|
51
|
+
* so a whole-clip watermark keeps hard edges. Fades shrink to half the window
|
|
52
|
+
* so a short window still reaches full opacity in the middle.
|
|
53
|
+
*/
|
|
54
|
+
export function burnWatermarkFades(startSeconds, endSeconds, durationSeconds, fadeSeconds) {
|
|
55
|
+
const start = Math.max(0, startSeconds ?? 0);
|
|
56
|
+
const clipEnd = endSeconds === undefined
|
|
57
|
+
? durationSeconds
|
|
58
|
+
: Math.min(endSeconds, durationSeconds);
|
|
59
|
+
const window = clipEnd - start;
|
|
60
|
+
if (!(fadeSeconds > 0) || !(window > 0))
|
|
61
|
+
return NO_FADES;
|
|
62
|
+
const fade = roundSeconds(Math.min(fadeSeconds, window / 2));
|
|
63
|
+
const endsEarly = endSeconds !== undefined
|
|
64
|
+
&& endSeconds < durationSeconds - CLIP_END_TOLERANCE_SECONDS;
|
|
65
|
+
return {
|
|
66
|
+
fadeIn: start > 0 ? fade : 0,
|
|
67
|
+
fadeOut: endsEarly ? fade : 0,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Alpha fades appended to a badge input chain. The badge input must be a
|
|
72
|
+
* timed stream (see `burnWatermarkInputArgs`) for the fade timestamps to
|
|
73
|
+
* line up with the main video.
|
|
74
|
+
*/
|
|
75
|
+
function burnWatermarkFadeFilters(fades, startSeconds, endSeconds) {
|
|
76
|
+
const parts = [];
|
|
77
|
+
if (fades.fadeIn > 0) {
|
|
78
|
+
parts.push(`fade=t=in:st=${startSeconds ?? 0}:d=${fades.fadeIn}:alpha=1`);
|
|
79
|
+
}
|
|
80
|
+
if (fades.fadeOut > 0 && endSeconds !== undefined) {
|
|
81
|
+
parts.push(`fade=t=out:st=${roundSeconds(endSeconds - fades.fadeOut)}:d=${fades.fadeOut}:alpha=1`);
|
|
82
|
+
}
|
|
83
|
+
return parts.map((part) => `,${part}`).join("");
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* How long the looped badge stream must run for its fades to complete: past
|
|
87
|
+
* the last ramp the badge is static, and overlay repeats the final frame for
|
|
88
|
+
* the rest of the clip. Null when the badge needs no fades and a single still
|
|
89
|
+
* frame does.
|
|
90
|
+
*/
|
|
91
|
+
export function burnWatermarkLoopSeconds(fades, startSeconds, endSeconds, durationSeconds) {
|
|
92
|
+
if (fades.fadeIn === 0 && fades.fadeOut === 0)
|
|
93
|
+
return null;
|
|
94
|
+
const settledAt = fades.fadeOut > 0 && endSeconds !== undefined
|
|
95
|
+
? endSeconds
|
|
96
|
+
: (startSeconds ?? 0) + fades.fadeIn;
|
|
97
|
+
return roundSeconds(Math.min(durationSeconds, settledAt + LOOP_MARGIN_SECONDS));
|
|
98
|
+
}
|
|
99
|
+
/** Input args for one badge image: a still frame, or a timed loop when it fades. */
|
|
100
|
+
export function burnWatermarkInputArgs(path, loopSeconds, frameRate) {
|
|
101
|
+
return loopSeconds === null
|
|
102
|
+
? ["-i", path]
|
|
103
|
+
: ["-loop", "1", "-framerate", frameRate, "-t", String(loopSeconds), "-i", path];
|
|
104
|
+
}
|
|
105
|
+
function burnWatermarkFrameRate(probe) {
|
|
106
|
+
const nominal = probe.videoStream?.video?.nominalFrameRate;
|
|
107
|
+
if (nominal && nominal.numerator > 0 && nominal.denominator > 0) {
|
|
108
|
+
return `${nominal.numerator}/${nominal.denominator}`;
|
|
109
|
+
}
|
|
110
|
+
return String(roundSeconds(probe.fps > 0 ? probe.fps : 30));
|
|
111
|
+
}
|
|
112
|
+
export function buildBurnWatermarkFilter(options) {
|
|
113
|
+
const minDim = Math.min(options.width, options.height);
|
|
114
|
+
const badgeHeight = Math.max(2, 2 * Math.round((minDim * WATERMARK_MIN_DIM_FRACTION * options.userScale) / 2));
|
|
115
|
+
const padding = Math.round(options.width * 0.02);
|
|
116
|
+
const { x, y } = burnOverlayPosition(options.corner, padding);
|
|
117
|
+
const handlePlacement = options.includeQr && options.handlePlacement
|
|
118
|
+
? options.handlePlacement
|
|
119
|
+
: "center";
|
|
120
|
+
const attachedToQr = handlePlacement !== "center";
|
|
121
|
+
const handleBadgeHeight = handlePillHeight(options.handlePaddingY ?? DEFAULT_HANDLE_PADDING);
|
|
122
|
+
const independentHandleHeight = Math.max(2, 2 * Math.round((badgeHeight * handleBadgeHeight / HANDLE_ONLY_BADGE_HEIGHT) / 2));
|
|
123
|
+
const attachedHandleHeight = Math.max(2, Math.round(badgeHeight * handleBadgeHeight / BADGE_WIDTH));
|
|
124
|
+
const handleScale = attachedToQr
|
|
125
|
+
? `${badgeHeight}:-2`
|
|
126
|
+
: `-2:${independentHandleHeight}`;
|
|
127
|
+
const handleOverlay = watermarkHandleOverlayPosition({
|
|
128
|
+
placement: handlePlacement,
|
|
129
|
+
corner: options.corner,
|
|
130
|
+
canvasWidth: options.width,
|
|
131
|
+
canvasHeight: options.height,
|
|
132
|
+
qrSize: badgeHeight,
|
|
133
|
+
handleWidth: attachedToQr
|
|
134
|
+
? badgeHeight
|
|
135
|
+
: Math.round(independentHandleHeight * BADGE_WIDTH / handleBadgeHeight),
|
|
136
|
+
handleHeight: attachedToQr ? attachedHandleHeight : independentHandleHeight,
|
|
137
|
+
padding,
|
|
138
|
+
handleVerticalPercent: options.handleVerticalPercent ?? DEFAULT_HANDLE_VERTICAL_PERCENT,
|
|
139
|
+
});
|
|
140
|
+
const sharedOpacity = options.opacity ?? WATERMARK_OPACITY;
|
|
141
|
+
const qrOpacity = options.qrOpacity ?? sharedOpacity;
|
|
142
|
+
const handleOpacity = options.handleOpacity ?? sharedOpacity;
|
|
143
|
+
const qrStart = options.qrStartSeconds ?? options.startSeconds;
|
|
144
|
+
const qrEnd = options.qrEndSeconds ?? options.endSeconds;
|
|
145
|
+
const handleStart = options.handleStartSeconds ?? options.startSeconds;
|
|
146
|
+
const handleEnd = options.handleEndSeconds ?? options.endSeconds;
|
|
147
|
+
const fadesFor = (start, end) => options.durationSeconds !== undefined && options.fadeSeconds !== undefined
|
|
148
|
+
? burnWatermarkFades(start, end, options.durationSeconds, options.fadeSeconds)
|
|
149
|
+
: NO_FADES;
|
|
150
|
+
const qrFade = burnWatermarkFadeFilters(fadesFor(qrStart, qrEnd), qrStart, qrEnd);
|
|
151
|
+
const handleFade = burnWatermarkFadeFilters(fadesFor(handleStart, handleEnd), handleStart, handleEnd);
|
|
152
|
+
const qrEnable = burnWatermarkEnable(qrStart, qrEnd);
|
|
153
|
+
const handleEnable = burnWatermarkEnable(handleStart, handleEnd);
|
|
154
|
+
const handleInput = options.includeQr ? 2 : 1;
|
|
155
|
+
if (!options.includeQr && !options.includeHandle) {
|
|
156
|
+
throw new Error("QR or handle watermark input is required");
|
|
157
|
+
}
|
|
158
|
+
const filters = [];
|
|
159
|
+
if (options.includeQr) {
|
|
160
|
+
filters.push(`[1:v]scale=-2:${badgeHeight},format=rgba,colorchannelmixer=aa=${qrOpacity}${qrFade}[qr]`);
|
|
161
|
+
}
|
|
162
|
+
if (options.includeHandle) {
|
|
163
|
+
filters.push(`[${handleInput}:v]scale=${handleScale},format=rgba,colorchannelmixer=aa=${handleOpacity}${handleFade}[handle]`);
|
|
164
|
+
}
|
|
165
|
+
if (options.includeQr) {
|
|
166
|
+
const output = options.includeHandle ? "withqr" : "vout";
|
|
167
|
+
filters.push(`[0:v][qr]overlay=${x}:${y}:format=auto${qrEnable}[${output}]`);
|
|
168
|
+
}
|
|
169
|
+
if (options.includeHandle) {
|
|
170
|
+
const base = options.includeQr ? "withqr" : "0:v";
|
|
171
|
+
filters.push(`[${base}][handle]overlay=${handleOverlay.x}:${handleOverlay.y}:format=auto${handleEnable}[vout]`);
|
|
172
|
+
}
|
|
173
|
+
return filters.join(";");
|
|
174
|
+
}
|
|
175
|
+
function badgeInputArgs(path, timing, probe, fadeSeconds) {
|
|
176
|
+
const fades = burnWatermarkFades(timing.startSeconds, timing.endSeconds, probe.videoDuration, fadeSeconds);
|
|
177
|
+
const loopSeconds = burnWatermarkLoopSeconds(fades, timing.startSeconds, timing.endSeconds, probe.videoDuration);
|
|
178
|
+
return burnWatermarkInputArgs(path, loopSeconds, burnWatermarkFrameRate(probe));
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Resolve the QR corner inside the job: a deferred vision pass when the tool
|
|
182
|
+
* asked for one, otherwise the corner the tool already settled on. Vision
|
|
183
|
+
* failures degrade to the tool's fallback rather than failing the burn.
|
|
184
|
+
*/
|
|
185
|
+
async function resolveBurnCorner(engine, payload, probe, signal) {
|
|
186
|
+
const fallback = {
|
|
187
|
+
corner: payload.corner,
|
|
188
|
+
placementSource: payload.placementSource ?? "explicit",
|
|
189
|
+
};
|
|
190
|
+
if (!payload.placement)
|
|
191
|
+
return fallback;
|
|
192
|
+
try {
|
|
193
|
+
const analysis = await analyzeOsgPlacement(engine, {
|
|
194
|
+
assetId: payload.assetId,
|
|
195
|
+
fingerprint: payload.placement.fingerprint,
|
|
196
|
+
userScale: payload.userScale,
|
|
197
|
+
badgeAspect: payload.placement.badgeAspect,
|
|
198
|
+
signal,
|
|
199
|
+
});
|
|
200
|
+
// Score corners against the source frame itself — a burned watermark
|
|
201
|
+
// lives in source pixels, so one corner serves every orientation.
|
|
202
|
+
const corner = scoreWatermarkCornersForCanvas(probe.width, probe.height, analysis.regions, {
|
|
203
|
+
aspect: payload.placement.badgeAspect,
|
|
204
|
+
userScale: payload.userScale,
|
|
205
|
+
}).position;
|
|
206
|
+
return { corner, placementSource: "vision" };
|
|
207
|
+
}
|
|
208
|
+
catch (err) {
|
|
209
|
+
signal.throwIfAborted();
|
|
210
|
+
logger.warn("watermark-placement-fallback", { assetId: payload.assetId, error: err });
|
|
211
|
+
return fallback;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Composite the watermark badge onto the source video: scaled to the min-dim
|
|
216
|
+
* fraction the render-time overlay uses, faded to `opacity`, anchored at
|
|
217
|
+
* `corner` with the standard edge padding, and faded in/out at any edge that
|
|
218
|
+
* falls inside the clip. Mirrors the trim/crop shape: replace the canonical
|
|
219
|
+
* file in place, drop stale visual analysis, reprocess.
|
|
220
|
+
*/
|
|
221
|
+
export function registerBurnWatermarkJob(engine) {
|
|
222
|
+
registerJobHandler("burn_watermark", async (rawPayload, ctx) => {
|
|
223
|
+
const payload = rawPayload;
|
|
224
|
+
const { assetId, assetDir, bookId, videoPath, qrPath, handlePath, opacity, userScale, qrOpacity, handleOpacity, handleVerticalPercent = DEFAULT_HANDLE_VERTICAL_PERCENT, handlePlacement = "center", handlePaddingY = DEFAULT_HANDLE_PADDING, fadeSeconds = DEFAULT_WATERMARK_FADE_SECONDS, startSeconds, endSeconds, qrStartSeconds, qrEndSeconds, handleStartSeconds, handleEndSeconds, } = payload;
|
|
225
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "process", { durationMs: LOCK_DURATION_MS, data: { state: "burning-watermark" } }, async () => {
|
|
226
|
+
const probe = await probeVideo(videoPath, { signal: ctx.signal });
|
|
227
|
+
const handleTiming = handlePath
|
|
228
|
+
? effectiveWatermarkTiming("Handle watermark", probe.videoDuration, handleStartSeconds ?? startSeconds, handleEndSeconds ?? endSeconds)
|
|
229
|
+
: {};
|
|
230
|
+
const qrTiming = qrPath
|
|
231
|
+
? effectiveWatermarkTiming("QR watermark", probe.videoDuration, qrStartSeconds ?? startSeconds, qrEndSeconds ?? endSeconds)
|
|
232
|
+
: {};
|
|
233
|
+
const placement = await resolveBurnCorner(engine, payload, probe, ctx.signal);
|
|
234
|
+
const outputPath = join(assetDir, "original_watermarked.mp4");
|
|
235
|
+
const filter = buildBurnWatermarkFilter({
|
|
236
|
+
width: probe.width,
|
|
237
|
+
height: probe.height,
|
|
238
|
+
corner: placement.corner,
|
|
239
|
+
opacity,
|
|
240
|
+
qrOpacity,
|
|
241
|
+
handleOpacity,
|
|
242
|
+
userScale,
|
|
243
|
+
includeQr: qrPath !== undefined,
|
|
244
|
+
includeHandle: handlePath !== undefined,
|
|
245
|
+
handleVerticalPercent,
|
|
246
|
+
handlePlacement,
|
|
247
|
+
handlePaddingY,
|
|
248
|
+
durationSeconds: probe.videoDuration,
|
|
249
|
+
fadeSeconds,
|
|
250
|
+
...(qrTiming.startSeconds !== undefined
|
|
251
|
+
? { qrStartSeconds: qrTiming.startSeconds }
|
|
252
|
+
: {}),
|
|
253
|
+
...(qrTiming.endSeconds !== undefined
|
|
254
|
+
? { qrEndSeconds: qrTiming.endSeconds }
|
|
255
|
+
: {}),
|
|
256
|
+
...(handleTiming.startSeconds !== undefined
|
|
257
|
+
? { handleStartSeconds: handleTiming.startSeconds }
|
|
258
|
+
: {}),
|
|
259
|
+
...(handleTiming.endSeconds !== undefined
|
|
260
|
+
? { handleEndSeconds: handleTiming.endSeconds }
|
|
261
|
+
: {}),
|
|
262
|
+
});
|
|
263
|
+
const watermarkInputs = [
|
|
264
|
+
...(qrPath ? badgeInputArgs(qrPath, qrTiming, probe, fadeSeconds) : []),
|
|
265
|
+
...(handlePath ? badgeInputArgs(handlePath, handleTiming, probe, fadeSeconds) : []),
|
|
266
|
+
];
|
|
267
|
+
try {
|
|
268
|
+
await runFfmpeg([
|
|
269
|
+
"-y",
|
|
270
|
+
"-i", videoPath,
|
|
271
|
+
...watermarkInputs,
|
|
272
|
+
"-filter_complex", filter,
|
|
273
|
+
"-map", "[vout]",
|
|
274
|
+
"-map", "0:a?",
|
|
275
|
+
...H264_MEDIUM,
|
|
276
|
+
"-pix_fmt", "yuv420p",
|
|
277
|
+
...AUDIO_OPTS,
|
|
278
|
+
outputPath,
|
|
279
|
+
], { signal: ctx.signal });
|
|
280
|
+
const check = await probeVideo(outputPath, { signal: ctx.signal });
|
|
281
|
+
if (check.width !== probe.width || check.height !== probe.height
|
|
282
|
+
|| Math.abs(check.duration - probe.duration) > 0.5) {
|
|
283
|
+
throw new Error(`Watermarked output looks wrong: ${check.width}x${check.height} ${check.duration.toFixed(2)}s`
|
|
284
|
+
+ ` from ${probe.width}x${probe.height} ${probe.duration.toFixed(2)}s`);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
catch (err) {
|
|
288
|
+
await rm(outputPath, { force: true });
|
|
289
|
+
throw err;
|
|
290
|
+
}
|
|
291
|
+
const inputFilename = basename(videoPath);
|
|
292
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
293
|
+
// Visual analysis now describes the un-watermarked frames; transcripts
|
|
294
|
+
// stay valid (audio and timing are untouched).
|
|
295
|
+
await engine.files.delete(assetId, ".original.analysis.json");
|
|
296
|
+
return { videoPath: join(assetDir, inputFilename), placement };
|
|
297
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
298
|
+
// The badge now lives in the pixels. The forced reprocess drops every
|
|
299
|
+
// stored render-time overlay setting, so a later render can't stamp it
|
|
300
|
+
// twice.
|
|
301
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
302
|
+
type: "process_video",
|
|
303
|
+
videoPath: result.videoPath,
|
|
304
|
+
assetId,
|
|
305
|
+
assetDir,
|
|
306
|
+
bookId,
|
|
307
|
+
provenance: { origin: "watermark", watermarked_at: new Date().toISOString() },
|
|
308
|
+
force: true,
|
|
309
|
+
});
|
|
310
|
+
return {
|
|
311
|
+
success: true,
|
|
312
|
+
corner: result.placement.corner,
|
|
313
|
+
placementSource: result.placement.placementSource,
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { basename, join } from "node:path";
|
|
5
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
6
|
+
import { getFFmpeggy, probeVideo, FFPROBE, H264_MEDIUM, AUDIO_OPTS, } from "../ffmpeg.js";
|
|
7
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
8
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const LOCK_DURATION_MS = 300_000;
|
|
11
|
+
const ATEMPO_MIN = 0.5;
|
|
12
|
+
// The muxer can pad the final frame's display duration past the ideal end
|
|
13
|
+
// (observed: 1.5 source frame intervals on B-frame sources); keep the
|
|
14
|
+
// duration check as a backstop and let frame-count equality carry precision.
|
|
15
|
+
const DURATION_FRAME_TOLERANCE = 1.5;
|
|
16
|
+
const DURATION_SLACK_S = 0.03;
|
|
17
|
+
const AUDIO_TOLERANCE_S = 0.15;
|
|
18
|
+
function buildAtempoChain(speed) {
|
|
19
|
+
const filters = [];
|
|
20
|
+
let remaining = speed;
|
|
21
|
+
while (remaining < ATEMPO_MIN) {
|
|
22
|
+
filters.push(`atempo=${ATEMPO_MIN}`);
|
|
23
|
+
remaining /= ATEMPO_MIN;
|
|
24
|
+
}
|
|
25
|
+
filters.push(`atempo=${remaining}`);
|
|
26
|
+
return filters;
|
|
27
|
+
}
|
|
28
|
+
function buildSpeedOutputOptions(speed, hasAudio, targetDuration, outputFrameRate) {
|
|
29
|
+
const videoChain = [`settb=AVTB`, `setpts=(PTS-STARTPTS)/${speed}`];
|
|
30
|
+
const audioChain = ["asetpts=PTS-STARTPTS", ...buildAtempoChain(speed)];
|
|
31
|
+
if (targetDuration !== undefined) {
|
|
32
|
+
const cap = targetDuration.toFixed(6);
|
|
33
|
+
videoChain.push(`trim=end=${cap}`);
|
|
34
|
+
audioChain.push(`atrim=end=${cap}`);
|
|
35
|
+
}
|
|
36
|
+
const opts = [
|
|
37
|
+
"-fps_mode:v", outputFrameRate === undefined ? "passthrough" : "cfr",
|
|
38
|
+
...(outputFrameRate === undefined ? [] : ["-r", String(outputFrameRate)]),
|
|
39
|
+
"-filter:v", videoChain.join(","),
|
|
40
|
+
];
|
|
41
|
+
if (hasAudio) {
|
|
42
|
+
opts.push("-filter:a", audioChain.join(","));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
opts.push("-an");
|
|
46
|
+
}
|
|
47
|
+
opts.push(...H264_MEDIUM, ...AUDIO_OPTS);
|
|
48
|
+
return opts;
|
|
49
|
+
}
|
|
50
|
+
async function probeVideoFrameCount(filePath) {
|
|
51
|
+
try {
|
|
52
|
+
const { stdout } = await execFileAsync(FFPROBE, [
|
|
53
|
+
"-v", "error",
|
|
54
|
+
"-select_streams", "v:0",
|
|
55
|
+
"-show_entries", "stream=nb_frames",
|
|
56
|
+
"-of", "json",
|
|
57
|
+
filePath,
|
|
58
|
+
]);
|
|
59
|
+
const data = JSON.parse(stdout);
|
|
60
|
+
const count = Number(data.streams?.[0]?.nb_frames);
|
|
61
|
+
return Number.isSafeInteger(count) && count > 0 ? count : null;
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async function hasConstantFrameSpacing(filePath) {
|
|
68
|
+
try {
|
|
69
|
+
const { stdout } = await execFileAsync(FFPROBE, [
|
|
70
|
+
"-v", "error",
|
|
71
|
+
"-select_streams", "v:0",
|
|
72
|
+
"-show_entries", "packet=pts",
|
|
73
|
+
"-of", "json",
|
|
74
|
+
filePath,
|
|
75
|
+
], { maxBuffer: 16 * 1024 * 1024 });
|
|
76
|
+
const data = JSON.parse(stdout);
|
|
77
|
+
const times = data.packets?.map((packet) => Number(packet.pts)) ?? [];
|
|
78
|
+
if (times.length < 2 || times.some((time) => !Number.isSafeInteger(time)))
|
|
79
|
+
return false;
|
|
80
|
+
times.sort((a, b) => a - b);
|
|
81
|
+
const interval = (times[times.length - 1] - times[0]) / (times.length - 1);
|
|
82
|
+
return interval > 0 && times.slice(1).every((time, index) => (time > times[index] && Math.abs(time - times[index] - interval) <= 1));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
return false;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
function retimeValidationErrors(input, output, speed, targetDuration) {
|
|
89
|
+
const errors = [];
|
|
90
|
+
if (output.videoCodec === null || output.videoDuration <= 0) {
|
|
91
|
+
errors.push("output has no playable video stream");
|
|
92
|
+
return errors;
|
|
93
|
+
}
|
|
94
|
+
// In exact-duration mode the trim/atrim cap wins when a stream would
|
|
95
|
+
// overrun the target, so expectations clamp to the cap.
|
|
96
|
+
const cap = targetDuration ?? Number.POSITIVE_INFINITY;
|
|
97
|
+
const frameInterval = input.fps > 0 ? 1 / input.fps : 1 / 30;
|
|
98
|
+
const videoTolerance = DURATION_FRAME_TOLERANCE * frameInterval / speed + DURATION_SLACK_S;
|
|
99
|
+
const expectedVideo = Math.min(input.videoDuration / speed, cap);
|
|
100
|
+
if (Math.abs(output.videoDuration - expectedVideo) > videoTolerance) {
|
|
101
|
+
errors.push(`video duration ${output.videoDuration.toFixed(3)}s does not match expected`
|
|
102
|
+
+ ` ${expectedVideo.toFixed(3)}s (source ${input.videoDuration.toFixed(3)}s at ${speed}x)`);
|
|
103
|
+
}
|
|
104
|
+
if (input.hasAudio) {
|
|
105
|
+
if (!output.hasAudio || output.audioDuration === null) {
|
|
106
|
+
errors.push("output lost the source audio track");
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
const expectedAudio = Math.min((input.audioDuration ?? input.videoDuration) / speed, cap);
|
|
110
|
+
if (Math.abs(output.audioDuration - expectedAudio) > AUDIO_TOLERANCE_S) {
|
|
111
|
+
errors.push(`audio duration ${output.audioDuration.toFixed(3)}s does not match expected`
|
|
112
|
+
+ ` ${expectedAudio.toFixed(3)}s (source audio retimed at ${speed}x)`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return errors;
|
|
117
|
+
}
|
|
118
|
+
export function registerChangeVideoSpeedJob(engine) {
|
|
119
|
+
registerJobHandler("change_video_speed", async (payload, ctx) => {
|
|
120
|
+
const { inputPath, outputPath, speed, targetDuration, hasAudio, assetId, assetDir, bookId, } = payload;
|
|
121
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "change_speed", { durationMs: LOCK_DURATION_MS, data: { state: "changing-speed" } }, async () => {
|
|
122
|
+
const inputProbe = await probeVideo(inputPath);
|
|
123
|
+
const sourceFrameCount = await probeVideoFrameCount(inputPath);
|
|
124
|
+
const outputFrameRate = sourceFrameCount !== null && inputProbe.videoDuration > 0
|
|
125
|
+
&& await hasConstantFrameSpacing(inputPath)
|
|
126
|
+
? sourceFrameCount / (targetDuration ?? inputProbe.videoDuration / speed)
|
|
127
|
+
: undefined;
|
|
128
|
+
const outputOptions = buildSpeedOutputOptions(speed, hasAudio, targetDuration, outputFrameRate);
|
|
129
|
+
const FFmpeggy = await getFFmpeggy();
|
|
130
|
+
const ff = new FFmpeggy({
|
|
131
|
+
input: inputPath,
|
|
132
|
+
output: outputPath,
|
|
133
|
+
outputOptions,
|
|
134
|
+
overwriteExisting: true,
|
|
135
|
+
});
|
|
136
|
+
await ff.run();
|
|
137
|
+
await ff.done();
|
|
138
|
+
// Validate before touching canonical media: a retime that dropped
|
|
139
|
+
// frames, drifted from the expected duration, or lost the audio track
|
|
140
|
+
// must fail the job and leave the current version recoverable.
|
|
141
|
+
const outputProbe = await probeVideo(outputPath);
|
|
142
|
+
const errors = retimeValidationErrors(inputProbe, outputProbe, speed, targetDuration);
|
|
143
|
+
const outputFrameCount = await probeVideoFrameCount(outputPath);
|
|
144
|
+
if (sourceFrameCount !== null
|
|
145
|
+
&& outputFrameCount !== null
|
|
146
|
+
&& sourceFrameCount !== outputFrameCount) {
|
|
147
|
+
errors.push(`video frame count ${outputFrameCount} does not match source ${sourceFrameCount}`);
|
|
148
|
+
}
|
|
149
|
+
if (errors.length > 0) {
|
|
150
|
+
await rm(outputPath, { force: true });
|
|
151
|
+
throw new Error(`change_video_speed produced invalid output for ${assetId}: ${errors.join("; ")}`);
|
|
152
|
+
}
|
|
153
|
+
const inputFilename = basename(inputPath);
|
|
154
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
155
|
+
const videoPath = join(assetDir, inputFilename);
|
|
156
|
+
return { videoPath };
|
|
157
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
158
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
159
|
+
type: "process_video",
|
|
160
|
+
videoPath: result.videoPath,
|
|
161
|
+
assetId,
|
|
162
|
+
assetDir,
|
|
163
|
+
bookId,
|
|
164
|
+
provenance: {
|
|
165
|
+
origin: "speed_change",
|
|
166
|
+
source_asset_id: assetId,
|
|
167
|
+
speed,
|
|
168
|
+
targetDuration,
|
|
169
|
+
speed_changed_at: new Date().toISOString(),
|
|
170
|
+
},
|
|
171
|
+
force: true,
|
|
172
|
+
skipDownstreamJobs: true,
|
|
173
|
+
});
|
|
174
|
+
return { success: true };
|
|
175
|
+
});
|
|
176
|
+
}
|