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,100 @@
|
|
|
1
|
+
import { registerJobHandler } from "../queue.js";
|
|
2
|
+
async function statusOf(engine, assetId) {
|
|
3
|
+
const res = await engine.status.get(assetId);
|
|
4
|
+
return res.ok ? res.value : "error";
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Restore one asset to its last usable state before the current error.
|
|
8
|
+
*
|
|
9
|
+
* Runs lock-free, exactly like restore_asset_version: recoverAssetRow and
|
|
10
|
+
* restoreAsset re-derive the asset row themselves, and a held lock would make
|
|
11
|
+
* recoverAssetRow treat the asset as "abandoned". Concurrency is guarded by a
|
|
12
|
+
* read-only check for a live work lease instead.
|
|
13
|
+
*
|
|
14
|
+
* Tier 1 (non-destructive): clear the durable failure signals — generation
|
|
15
|
+
* error, stale pending task, expired lock — then re-derive the row from disk.
|
|
16
|
+
* If the media on disk is intact, this *is* the last usable state and no file
|
|
17
|
+
* changes are needed.
|
|
18
|
+
*
|
|
19
|
+
* Tier 2 (Dolt restore): the current manifest itself is broken, so walk the
|
|
20
|
+
* asset history newest-first and restore the first revision that yields a `ready`
|
|
21
|
+
* asset. If none do, put the asset back on the revision the fix started from
|
|
22
|
+
* so a failed walk cannot delete the only copy of the media.
|
|
23
|
+
*/
|
|
24
|
+
async function fixAsset(engine, assetId) {
|
|
25
|
+
// Refuse to touch an asset a job is actively working (live, unexpired lease).
|
|
26
|
+
const work = await engine.jobs.artifactWork.read(assetId);
|
|
27
|
+
if (work.ok &&
|
|
28
|
+
work.value &&
|
|
29
|
+
work.value.status === "working" &&
|
|
30
|
+
work.value.deadlineAt != null &&
|
|
31
|
+
work.value.deadlineAt > Date.now()) {
|
|
32
|
+
return { status: "busy", asset_id: assetId, reason: "asset has an active job lease" };
|
|
33
|
+
}
|
|
34
|
+
const before = await statusOf(engine, assetId);
|
|
35
|
+
// Tier 1 — clear durable failure signals, then re-derive from disk.
|
|
36
|
+
await engine.jobs.failures.clear(assetId);
|
|
37
|
+
const pending = await engine.jobs.pending.read(assetId);
|
|
38
|
+
if (pending.ok && pending.value) {
|
|
39
|
+
// delete (not forceFail) so we don't write a fresh generation error.
|
|
40
|
+
await engine.jobs.pending.delete(assetId);
|
|
41
|
+
}
|
|
42
|
+
const dirRes = await engine.workspaces.resolveArtifact(assetId);
|
|
43
|
+
if (dirRes.ok)
|
|
44
|
+
await engine.jobs.locks.cleanStale(dirRes.value);
|
|
45
|
+
await engine.jobs.recoverArtifact(assetId);
|
|
46
|
+
const afterTier1 = await statusOf(engine, assetId);
|
|
47
|
+
if (afterTier1 === "ready") {
|
|
48
|
+
return {
|
|
49
|
+
status: before === "ready" ? "already_healthy" : "recovered_in_place",
|
|
50
|
+
asset_id: assetId,
|
|
51
|
+
asset_status: afterTier1,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
// Tier 2 — restore from immutable Dolt history using stable identity.
|
|
55
|
+
const artifact = engine.artifacts.get(assetId);
|
|
56
|
+
if (!artifact.ok) {
|
|
57
|
+
return {
|
|
58
|
+
status: "unfixable",
|
|
59
|
+
asset_id: assetId,
|
|
60
|
+
scanned: 0,
|
|
61
|
+
asset_status: afterTier1,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
const baseline = engine.head;
|
|
65
|
+
const history = engine.history.artifact(artifact.value.artifactId);
|
|
66
|
+
for (const commit of history) {
|
|
67
|
+
if (commit.hash === baseline)
|
|
68
|
+
continue;
|
|
69
|
+
const restored = await engine.history.restoreArtifact(artifact.value.artifactId, commit.hash);
|
|
70
|
+
if (!restored.ok)
|
|
71
|
+
continue;
|
|
72
|
+
// restoreAsset re-derives the row; clear any error it carried forward.
|
|
73
|
+
await engine.jobs.failures.clear(assetId);
|
|
74
|
+
const status = await statusOf(engine, assetId);
|
|
75
|
+
if (status === "ready") {
|
|
76
|
+
return {
|
|
77
|
+
status: "restored",
|
|
78
|
+
asset_id: assetId,
|
|
79
|
+
from_commit: commit.hash,
|
|
80
|
+
commit_date: commit.date,
|
|
81
|
+
asset_status: status,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (engine.head !== baseline) {
|
|
86
|
+
await engine.history.restoreArtifact(artifact.value.artifactId, baseline);
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
status: "unfixable",
|
|
90
|
+
asset_id: assetId,
|
|
91
|
+
scanned: history.length,
|
|
92
|
+
asset_status: await statusOf(engine, assetId),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
export function registerDoctorFixJob(engine) {
|
|
96
|
+
registerJobHandler("doctor_fix", async (payload) => {
|
|
97
|
+
const { assetId } = payload;
|
|
98
|
+
return fixAsset(engine, assetId);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { stat, unlink } from "node:fs/promises";
|
|
3
|
+
import { YtDlp } from "ytdlp-nodejs";
|
|
4
|
+
import { ytdlpRunOptions } from "../ytdlp-binary.js";
|
|
5
|
+
import { extractOpenGraph } from "@devmehq/open-graph-extractor";
|
|
6
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
7
|
+
import { runFfmpeg, probeVideo, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
|
|
8
|
+
import { downloadUrlIntoAsset } from "./_download-into-asset.js";
|
|
9
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
10
|
+
import { ingestWorkspaceOutputs, replaceArtifactFileFromPath, } from "./_persist-output.js";
|
|
11
|
+
import { assertSaneTrimOutput, trimRange, trimRangeExact, } from "./trim-video.js";
|
|
12
|
+
const LOCK_DURATION_MS = 600_000;
|
|
13
|
+
// Hidden filename so a failed video download doesn't leave behind an
|
|
14
|
+
// `original.jpg` that sync's `findUnprocessedUploads` would pick up and
|
|
15
|
+
// process as if the asset were a successful image upload (origin "external").
|
|
16
|
+
const OG_IMAGE_FILENAME = ".opengraph.jpg";
|
|
17
|
+
const OG_PLATFORM_SUFFIX = / on (?:X|Twitter|Instagram|Facebook|YouTube|TikTok|Vimeo|Reddit|Twitch|Dailymotion)$/i;
|
|
18
|
+
const OG_HANDLE_PAREN = / \(@?\w+\)/g;
|
|
19
|
+
function cleanOgTitle(raw) {
|
|
20
|
+
return raw.replace(OG_PLATFORM_SUFFIX, "").replace(OG_HANDLE_PAREN, "").trim();
|
|
21
|
+
}
|
|
22
|
+
export async function validateDownloadOutput(outputPath) {
|
|
23
|
+
const partPath = outputPath + ".part";
|
|
24
|
+
let fileStat = null;
|
|
25
|
+
try {
|
|
26
|
+
fileStat = await stat(outputPath);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// output file doesn't exist — check for leftover .part
|
|
30
|
+
}
|
|
31
|
+
if (fileStat) {
|
|
32
|
+
if (fileStat.size === 0) {
|
|
33
|
+
try {
|
|
34
|
+
await unlink(outputPath);
|
|
35
|
+
}
|
|
36
|
+
catch { /* best-effort cleanup */ }
|
|
37
|
+
throw new Error("Download produced an empty file");
|
|
38
|
+
}
|
|
39
|
+
// yt-dlp can finalize a non-empty mp4 that is structurally broken.
|
|
40
|
+
// Probe the container so process_video doesn't get a non-decodable file.
|
|
41
|
+
try {
|
|
42
|
+
await probeVideo(outputPath);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
try {
|
|
46
|
+
await unlink(outputPath);
|
|
47
|
+
}
|
|
48
|
+
catch { /* best-effort cleanup */ }
|
|
49
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
50
|
+
throw new Error(`Download produced an unreadable video: ${msg}`);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Output file missing — check for .part file
|
|
55
|
+
let hasPartFile = false;
|
|
56
|
+
try {
|
|
57
|
+
await stat(partPath);
|
|
58
|
+
hasPartFile = true;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// no .part file either
|
|
62
|
+
}
|
|
63
|
+
if (hasPartFile) {
|
|
64
|
+
try {
|
|
65
|
+
await unlink(partPath);
|
|
66
|
+
}
|
|
67
|
+
catch { /* best-effort cleanup */ }
|
|
68
|
+
throw new Error("Download incomplete: only a partial .part file was found");
|
|
69
|
+
}
|
|
70
|
+
throw new Error("Download failed: output file was not created");
|
|
71
|
+
}
|
|
72
|
+
async function blurFillExport(inputPath, outputPath, targetWidth, targetHeight) {
|
|
73
|
+
const filterComplex = [
|
|
74
|
+
`[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=increase,crop=${targetWidth}:${targetHeight},boxblur=20:5[bg]`,
|
|
75
|
+
`[0:v]scale=${targetWidth}:${targetHeight}:force_original_aspect_ratio=decrease[fg]`,
|
|
76
|
+
`[bg][fg]overlay=(W-w)/2:(H-h)/2`,
|
|
77
|
+
].join(";");
|
|
78
|
+
await runFfmpeg([
|
|
79
|
+
"-y", "-i", inputPath,
|
|
80
|
+
"-filter_complex", filterComplex,
|
|
81
|
+
...H264_MEDIUM, ...AUDIO_OPTS,
|
|
82
|
+
outputPath,
|
|
83
|
+
]);
|
|
84
|
+
}
|
|
85
|
+
function extractOgImageUrl(ogImage) {
|
|
86
|
+
if (!ogImage)
|
|
87
|
+
return null;
|
|
88
|
+
if (typeof ogImage === "string")
|
|
89
|
+
return ogImage;
|
|
90
|
+
if (Array.isArray(ogImage)) {
|
|
91
|
+
const first = ogImage[0];
|
|
92
|
+
if (!first)
|
|
93
|
+
return null;
|
|
94
|
+
if (typeof first === "string")
|
|
95
|
+
return first;
|
|
96
|
+
return typeof first === "object" && "url" in first ? first.url : null;
|
|
97
|
+
}
|
|
98
|
+
return typeof ogImage === "object" && "url" in ogImage ? ogImage.url : null;
|
|
99
|
+
}
|
|
100
|
+
async function readOpenGraphData(engine, assetId) {
|
|
101
|
+
const result = await engine.files.read(assetId, ".opengraph.json");
|
|
102
|
+
if (!result.ok)
|
|
103
|
+
return null;
|
|
104
|
+
try {
|
|
105
|
+
return JSON.parse(result.value.toString());
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
async function loadOpenGraphData(engine, url, assetId) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await fetch(url, {
|
|
114
|
+
headers: { "User-Agent": "Mozilla/5.0 (compatible; VideobookBot/1.0)" },
|
|
115
|
+
signal: AbortSignal.timeout(10_000),
|
|
116
|
+
redirect: "follow",
|
|
117
|
+
});
|
|
118
|
+
const ogData = extractOpenGraph(await response.text());
|
|
119
|
+
await engine.files.write(assetId, ".opengraph.json", Buffer.from(JSON.stringify(ogData, null, 2)));
|
|
120
|
+
return ogData;
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return readOpenGraphData(engine, assetId);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
async function stageOpenGraphImage(engine, ogData, assetId) {
|
|
127
|
+
const imageUrl = extractOgImageUrl(ogData.ogImage);
|
|
128
|
+
if (!imageUrl)
|
|
129
|
+
return;
|
|
130
|
+
try {
|
|
131
|
+
await downloadUrlIntoAsset(engine, imageUrl, assetId, OG_IMAGE_FILENAME, {
|
|
132
|
+
kind: "OG image",
|
|
133
|
+
fetchInit: { signal: AbortSignal.timeout(15_000), redirect: "follow" },
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (error) {
|
|
137
|
+
void error;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const IMAGE_EXTENSIONS = new Set(["jpg", "jpeg", "png", "webp", "gif"]);
|
|
141
|
+
export async function applyRequestedDownloadRange(engine, assetId, assetDir, outputPath, requestedRange) {
|
|
142
|
+
const start = requestedRange.start ?? 0;
|
|
143
|
+
const end = requestedRange.end;
|
|
144
|
+
if (!Number.isFinite(start) || start < 0
|
|
145
|
+
|| (end !== undefined && (!Number.isFinite(end) || end <= start))) {
|
|
146
|
+
throw new Error("Requested download range is invalid");
|
|
147
|
+
}
|
|
148
|
+
if (start === 0 && end === undefined)
|
|
149
|
+
return false;
|
|
150
|
+
const rangeFilename = ".requested-range.mp4";
|
|
151
|
+
const rangePath = join(assetDir, rangeFilename);
|
|
152
|
+
try {
|
|
153
|
+
if (end !== undefined) {
|
|
154
|
+
await trimRangeExact(outputPath, rangePath, start, end - start);
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await trimRange(outputPath, rangePath, start, undefined);
|
|
158
|
+
}
|
|
159
|
+
await assertSaneTrimOutput(outputPath, rangePath);
|
|
160
|
+
await replaceArtifactFileFromPath(engine, assetId, "original.mp4", rangePath);
|
|
161
|
+
return true;
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
await engine.files.delete(assetId, rangeFilename);
|
|
165
|
+
throw error;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function isImageContent(info) {
|
|
169
|
+
if (!info.ext)
|
|
170
|
+
return false;
|
|
171
|
+
if (!IMAGE_EXTENSIONS.has(info.ext.toLowerCase()))
|
|
172
|
+
return false;
|
|
173
|
+
if (info.vcodec && info.vcodec !== "none")
|
|
174
|
+
return false;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
export function imageExtension(info) {
|
|
178
|
+
const ext = info.ext?.toLowerCase() ?? "jpg";
|
|
179
|
+
return IMAGE_EXTENSIONS.has(ext) ? `.${ext}` : ".jpg";
|
|
180
|
+
}
|
|
181
|
+
export function registerDownloadVideoJob(engine) {
|
|
182
|
+
registerJobHandler("download_video", async (payload, ctx) => {
|
|
183
|
+
const ytdlp = new YtDlp(ytdlpRunOptions());
|
|
184
|
+
const { url, assetId, assetDir, exportVideos, sourcePrefix, requestedRange, } = payload;
|
|
185
|
+
const bookId = payload.bookId ?? "";
|
|
186
|
+
const outputPath = join(assetDir, "original.mp4");
|
|
187
|
+
return withLockedLocalLease(engine, assetId, assetDir, ctx, "download", { durationMs: LOCK_DURATION_MS, data: { state: "downloading" } }, async () => {
|
|
188
|
+
// (a) Pre-stage OG image as a hidden file. The b2 fallback (yt-dlp
|
|
189
|
+
// returns "no video") promotes this to original.jpg on a fresh image
|
|
190
|
+
// asset; a successful video download promotes it after step (d).
|
|
191
|
+
const ogData = await loadOpenGraphData(engine, url, assetId);
|
|
192
|
+
if (ogData) {
|
|
193
|
+
await stageOpenGraphImage(engine, ogData, assetId);
|
|
194
|
+
}
|
|
195
|
+
// (b) Fetch ytdlp metadata
|
|
196
|
+
let info;
|
|
197
|
+
try {
|
|
198
|
+
info = await ytdlp.getInfoAsync(url);
|
|
199
|
+
}
|
|
200
|
+
catch (error) {
|
|
201
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
202
|
+
// (b2) yt-dlp found no video — fall back to OG image as an image asset
|
|
203
|
+
if (/no video/i.test(msg)) {
|
|
204
|
+
const ogImageResult = await engine.files.read(assetId, OG_IMAGE_FILENAME);
|
|
205
|
+
if (!ogImageResult.ok)
|
|
206
|
+
throw new Error(`No video found and no OG image available: ${msg}`);
|
|
207
|
+
// Read OG metadata for asset naming and provenance
|
|
208
|
+
let ogTitle = "image";
|
|
209
|
+
const fallbackOgData = ogData ?? await readOpenGraphData(engine, assetId);
|
|
210
|
+
if (fallbackOgData?.ogTitle)
|
|
211
|
+
ogTitle = cleanOgTitle(fallbackOgData.ogTitle);
|
|
212
|
+
const assetName = [sourcePrefix, ogTitle].join(" ");
|
|
213
|
+
const imgResult = await engine.artifacts.create("image", assetName);
|
|
214
|
+
if (!imgResult.ok)
|
|
215
|
+
throw new Error(`Failed to create image asset: ${imgResult.error.message}`);
|
|
216
|
+
const imgAssetId = imgResult.value.artifactId;
|
|
217
|
+
// Copy OG image and metadata to new image asset
|
|
218
|
+
await engine.files.write(imgAssetId, "original.jpg", ogImageResult.value);
|
|
219
|
+
try {
|
|
220
|
+
await engine.files.copy(assetId, ".opengraph.json", imgAssetId, ".opengraph.json");
|
|
221
|
+
}
|
|
222
|
+
catch { /* non-fatal */ }
|
|
223
|
+
// Release lock and delete the old video asset
|
|
224
|
+
await engine.jobs.locks.release(assetDir);
|
|
225
|
+
await engine.artifacts.delete(assetId);
|
|
226
|
+
const provenance = {
|
|
227
|
+
origin: "download",
|
|
228
|
+
source_url: url,
|
|
229
|
+
title: fallbackOgData?.ogTitle ?? "",
|
|
230
|
+
uploader: "",
|
|
231
|
+
description: fallbackOgData?.ogDescription ?? "",
|
|
232
|
+
webpage_url: url,
|
|
233
|
+
platform: sourcePrefix,
|
|
234
|
+
thumbnail: "",
|
|
235
|
+
downloaded_at: new Date().toISOString(),
|
|
236
|
+
};
|
|
237
|
+
const processJobId = `process-image-${imgAssetId}-${Date.now()}`;
|
|
238
|
+
await queue.enqueue(processJobId, {
|
|
239
|
+
type: "process_image",
|
|
240
|
+
assetId: imgAssetId,
|
|
241
|
+
bookId,
|
|
242
|
+
extension: ".jpg",
|
|
243
|
+
provenance,
|
|
244
|
+
});
|
|
245
|
+
return { success: true, retyped: true, assetId: imgAssetId };
|
|
246
|
+
}
|
|
247
|
+
throw new Error(`Failed to fetch video info: ${msg}`);
|
|
248
|
+
}
|
|
249
|
+
// (c) Handle playlists — if multiple entries, create additional assets + jobs for entries 2+
|
|
250
|
+
let playlistItem;
|
|
251
|
+
if (info._type === "playlist" && Array.isArray(info.entries) && info.entries.length > 1) {
|
|
252
|
+
// Current job handles entry 1
|
|
253
|
+
playlistItem = 1;
|
|
254
|
+
// Enqueue separate jobs for entries 2+
|
|
255
|
+
for (let i = 1; i < info.entries.length; i++) {
|
|
256
|
+
const entry = info.entries[i];
|
|
257
|
+
const entryKind = isImageContent(entry) ? "image" : "video";
|
|
258
|
+
const entryName = `${sourcePrefix} ${entry.title ?? `${entryKind} ${i + 1}`}`;
|
|
259
|
+
const assetResult = await engine.artifacts.create(entryKind, entryName);
|
|
260
|
+
if (!assetResult.ok)
|
|
261
|
+
continue;
|
|
262
|
+
const { artifactId: newAssetId, path: newAssetDir } = assetResult.value;
|
|
263
|
+
const newFilename = "original.mp4";
|
|
264
|
+
const newOutputPath = join(newAssetDir, newFilename);
|
|
265
|
+
const newJobId = `download-${newAssetId}-${Date.now()}`;
|
|
266
|
+
await queue.enqueue(newJobId, {
|
|
267
|
+
type: "download_video",
|
|
268
|
+
url,
|
|
269
|
+
outputPath: newOutputPath,
|
|
270
|
+
assetId: newAssetId,
|
|
271
|
+
assetDir: newAssetDir,
|
|
272
|
+
bookId,
|
|
273
|
+
exportVideos,
|
|
274
|
+
full: true,
|
|
275
|
+
sourcePrefix,
|
|
276
|
+
...(requestedRange ? { requestedRange } : {}),
|
|
277
|
+
_playlistItem: i + 1,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
else if (info._type === "playlist" && Array.isArray(info.entries) && info.entries.length === 1) {
|
|
282
|
+
playlistItem = 1;
|
|
283
|
+
}
|
|
284
|
+
// Check for internal _playlistItem (set when this job was spawned for a playlist entry)
|
|
285
|
+
const internalPlaylistItem = payload._playlistItem;
|
|
286
|
+
if (internalPlaylistItem !== undefined) {
|
|
287
|
+
playlistItem = internalPlaylistItem;
|
|
288
|
+
}
|
|
289
|
+
// (d0) Detect image-only content — retype asset and route to process_image
|
|
290
|
+
const itemInfo = (playlistItem !== undefined && info._type === "playlist" && info.entries?.length)
|
|
291
|
+
? (info.entries[playlistItem - 1] ?? info)
|
|
292
|
+
: info;
|
|
293
|
+
if (isImageContent(itemInfo)) {
|
|
294
|
+
let effectiveAssetId = assetId;
|
|
295
|
+
// Retype video asset to image if needed
|
|
296
|
+
const sourceArtifact = engine.artifacts.get(assetId);
|
|
297
|
+
if (sourceArtifact.ok && sourceArtifact.value.kind === "video") {
|
|
298
|
+
const assetName = [sourcePrefix, itemInfo.title ?? "image"].join(" ");
|
|
299
|
+
const imgResult = await engine.artifacts.create("image", assetName);
|
|
300
|
+
if (!imgResult.ok)
|
|
301
|
+
throw new Error(`Failed to create image asset: ${imgResult.error.message}`);
|
|
302
|
+
effectiveAssetId = imgResult.value.artifactId;
|
|
303
|
+
// Copy .opengraph.json to new asset
|
|
304
|
+
try {
|
|
305
|
+
await engine.files.copy(assetId, ".opengraph.json", effectiveAssetId, ".opengraph.json");
|
|
306
|
+
}
|
|
307
|
+
catch {
|
|
308
|
+
// non-fatal — OG data may not exist
|
|
309
|
+
}
|
|
310
|
+
// Release lock and delete the old video asset
|
|
311
|
+
await engine.jobs.locks.release(assetDir);
|
|
312
|
+
const delResult = await engine.artifacts.delete(assetId);
|
|
313
|
+
if (!delResult.ok) {
|
|
314
|
+
// non-fatal — an orphaned video asset is acceptable
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
// Download image directly from yt-dlp's resolved URL
|
|
318
|
+
const imageUrl = itemInfo.url ?? itemInfo.thumbnail ?? "";
|
|
319
|
+
if (!imageUrl)
|
|
320
|
+
throw new Error("No image URL found in content info");
|
|
321
|
+
const ext = imageExtension(itemInfo);
|
|
322
|
+
await downloadUrlIntoAsset(engine, imageUrl, effectiveAssetId, `original${ext}`, {
|
|
323
|
+
kind: "image",
|
|
324
|
+
fetchInit: { signal: AbortSignal.timeout(30_000), redirect: "follow" },
|
|
325
|
+
});
|
|
326
|
+
// Build provenance
|
|
327
|
+
const provenance = {
|
|
328
|
+
origin: "download",
|
|
329
|
+
source_url: url,
|
|
330
|
+
title: itemInfo.title ?? info.title ?? "",
|
|
331
|
+
uploader: itemInfo.uploader ?? info.uploader ?? "",
|
|
332
|
+
description: itemInfo.description ?? info.description ?? "",
|
|
333
|
+
webpage_url: itemInfo.webpage_url ?? info.webpage_url ?? "",
|
|
334
|
+
platform: itemInfo.extractor ?? info.extractor ?? "",
|
|
335
|
+
thumbnail: itemInfo.thumbnail ?? info.thumbnail ?? "",
|
|
336
|
+
downloaded_at: new Date().toISOString(),
|
|
337
|
+
};
|
|
338
|
+
// Enqueue process_image instead of process_video
|
|
339
|
+
const processJobId = `process-image-${effectiveAssetId}-${Date.now()}`;
|
|
340
|
+
await queue.enqueue(processJobId, {
|
|
341
|
+
type: "process_image",
|
|
342
|
+
assetId: effectiveAssetId,
|
|
343
|
+
bookId,
|
|
344
|
+
extension: ext,
|
|
345
|
+
provenance,
|
|
346
|
+
});
|
|
347
|
+
return { success: true, retyped: assetId !== effectiveAssetId, assetId: effectiveAssetId };
|
|
348
|
+
}
|
|
349
|
+
// (d) Regular ytdlp download
|
|
350
|
+
const builder = ytdlp
|
|
351
|
+
.download(url)
|
|
352
|
+
.setOutputTemplate(outputPath)
|
|
353
|
+
.addArgs("--merge-output-format", "mp4");
|
|
354
|
+
if (playlistItem !== undefined) {
|
|
355
|
+
builder.addArgs("--playlist-items", String(playlistItem));
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
builder.addArgs("--no-playlist");
|
|
359
|
+
}
|
|
360
|
+
await builder.run();
|
|
361
|
+
// (d2) Validate download output
|
|
362
|
+
await validateDownloadOutput(outputPath);
|
|
363
|
+
const rangeApplied = requestedRange
|
|
364
|
+
? await applyRequestedDownloadRange(engine, assetId, assetDir, outputPath, requestedRange)
|
|
365
|
+
: false;
|
|
366
|
+
if (rangeApplied)
|
|
367
|
+
await validateDownloadOutput(outputPath);
|
|
368
|
+
// (d3) Promote staged OG image to original.jpg so process_video uses
|
|
369
|
+
// the curated platform thumbnail (e.g. YouTube maxres) rather than a
|
|
370
|
+
// first-frame extraction. Best-effort — process_video falls back to a
|
|
371
|
+
// first-frame thumbnail when original.jpg is absent.
|
|
372
|
+
try {
|
|
373
|
+
const ogImageResult = await engine.files.read(assetId, OG_IMAGE_FILENAME);
|
|
374
|
+
if (ogImageResult.ok) {
|
|
375
|
+
await engine.files.write(assetId, "original.jpg", ogImageResult.value);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
catch { /* non-fatal */ }
|
|
379
|
+
// (e) Optional exports
|
|
380
|
+
const exports = [];
|
|
381
|
+
if (exportVideos) {
|
|
382
|
+
try {
|
|
383
|
+
const landscape = join(assetDir, "original_16x9.mp4");
|
|
384
|
+
await blurFillExport(outputPath, landscape, 1920, 1080);
|
|
385
|
+
exports.push("original_16x9.mp4");
|
|
386
|
+
}
|
|
387
|
+
catch {
|
|
388
|
+
// export failure is non-fatal
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
const portrait = join(assetDir, "original_9x16.mp4");
|
|
392
|
+
await blurFillExport(outputPath, portrait, 1080, 1920);
|
|
393
|
+
exports.push("original_9x16.mp4");
|
|
394
|
+
}
|
|
395
|
+
catch {
|
|
396
|
+
// export failure is non-fatal
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// (f) Build provenance from ytdlp metadata
|
|
400
|
+
const provenance = {
|
|
401
|
+
origin: "download",
|
|
402
|
+
source_url: url,
|
|
403
|
+
title: info.title ?? "",
|
|
404
|
+
uploader: info.uploader ?? "",
|
|
405
|
+
description: info.description ?? "",
|
|
406
|
+
webpage_url: info.webpage_url ?? "",
|
|
407
|
+
platform: info.extractor ?? "",
|
|
408
|
+
thumbnail: info.thumbnail ?? "",
|
|
409
|
+
full: !rangeApplied,
|
|
410
|
+
...(requestedRange ? { requested_range: requestedRange } : {}),
|
|
411
|
+
downloaded_at: new Date().toISOString(),
|
|
412
|
+
};
|
|
413
|
+
await ingestWorkspaceOutputs(engine, assetId, ["original.mp4", ...exports], "download_video", { sourceUrl: url, exports });
|
|
414
|
+
// (g) Enqueue process_video
|
|
415
|
+
const processJobId = `process-video-${assetId}-${Date.now()}`;
|
|
416
|
+
await queue.enqueue(processJobId, {
|
|
417
|
+
type: "process_video",
|
|
418
|
+
videoPath: outputPath,
|
|
419
|
+
assetId,
|
|
420
|
+
assetDir,
|
|
421
|
+
bookId,
|
|
422
|
+
provenance,
|
|
423
|
+
});
|
|
424
|
+
return { success: true, exports };
|
|
425
|
+
}, { durationMs: LOCK_DURATION_MS });
|
|
426
|
+
});
|
|
427
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { findPrimaryMediaFile } from "videobook-engine";
|
|
2
|
+
import { appendDiagnostic } from "../diagnostics.js";
|
|
3
|
+
import { enqueueOptions } from "../queue.js";
|
|
4
|
+
export async function enqueueSearchIndex(engine, artifactId) {
|
|
5
|
+
try {
|
|
6
|
+
const artifact = engine.artifacts.get(artifactId);
|
|
7
|
+
if (!artifact.ok)
|
|
8
|
+
throw new Error(artifact.error.message);
|
|
9
|
+
if (!["image", "video", "audio", "final"].includes(artifact.value.kind))
|
|
10
|
+
return;
|
|
11
|
+
const manifest = await engine.withLocalMedia(() => engine.files.manifest(artifactId));
|
|
12
|
+
if (!manifest.ok)
|
|
13
|
+
throw Object.assign(new Error(manifest.error.message), { name: manifest.error.code });
|
|
14
|
+
const source = findPrimaryMediaFile(manifest.value.files, artifact.value.kind);
|
|
15
|
+
if (!source)
|
|
16
|
+
throw new Error("No media source is available for indexing");
|
|
17
|
+
engine.jobs.queue.enqueue(enqueueOptions(`semantic-index:${artifactId}:${source.objectHash}`, {
|
|
18
|
+
type: "semantic_index", prepareVisualMedia: true, assetId: artifactId, bookId: engine.book.get().bookId,
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
await appendDiagnostic(engine, "semantic-index-deferred", { artifactId, error }).catch(() => undefined);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join, relative } from "node:path";
|
|
4
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
5
|
+
import { trimRange, trimRangeExact, assertSaneTrimOutput } from "./trim-video.js";
|
|
6
|
+
import { ingestWorkspaceOutputs } from "./_persist-output.js";
|
|
7
|
+
import { validateMediaOutputPaths, withMediaOutputLock } from "../media-output-path.js";
|
|
8
|
+
async function buildChunk(engine, args) {
|
|
9
|
+
const { sourcePath, outputPath, start, end, assetId, provenance, exact, skipMetadata } = args;
|
|
10
|
+
try {
|
|
11
|
+
if (exact) {
|
|
12
|
+
await trimRangeExact(sourcePath, outputPath, start, end - start);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
await trimRange(sourcePath, outputPath, start, end);
|
|
16
|
+
}
|
|
17
|
+
await assertSaneTrimOutput(sourcePath, outputPath);
|
|
18
|
+
}
|
|
19
|
+
catch (err) {
|
|
20
|
+
await rm(outputPath, { force: true });
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
if (!skipMetadata) {
|
|
24
|
+
await ingestWorkspaceOutputs(engine, assetId, [outputPath.split("/").pop()], "even_split_chunk", provenance);
|
|
25
|
+
await engine.metadata.artifacts.write(assetId, "original", provenance);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async function splitIntoDestination(engine, payload, ctx) {
|
|
29
|
+
const assetId = payload.destination.assetId;
|
|
30
|
+
const manifest = await engine.files.manifest(assetId);
|
|
31
|
+
if (!manifest.ok)
|
|
32
|
+
throw new Error(manifest.error.message);
|
|
33
|
+
const assetDir = manifest.value.path;
|
|
34
|
+
return withMediaOutputLock(engine, assetDir, ctx.signal, async () => {
|
|
35
|
+
const filenames = await validateMediaOutputPaths(engine, assetId, payload.chunks.map((chunk) => relative(assetDir, chunk.outputPath)), ".mp4");
|
|
36
|
+
const workspace = await mkdtemp(join(tmpdir(), "videobook-split-destination-"));
|
|
37
|
+
try {
|
|
38
|
+
const outputs = payload.chunks.map((_, index) => join(workspace, `${index}.mp4`));
|
|
39
|
+
const built = await Promise.allSettled(payload.chunks.map((chunk, index) => buildChunk(engine, {
|
|
40
|
+
sourcePath: payload.sourcePath,
|
|
41
|
+
outputPath: outputs[index],
|
|
42
|
+
start: chunk.start,
|
|
43
|
+
end: chunk.end,
|
|
44
|
+
assetId,
|
|
45
|
+
provenance: {},
|
|
46
|
+
exact: chunk.exact,
|
|
47
|
+
skipMetadata: true,
|
|
48
|
+
})));
|
|
49
|
+
const failed = built.find((result) => result.status === "rejected");
|
|
50
|
+
if (failed?.status === "rejected")
|
|
51
|
+
throw failed.reason;
|
|
52
|
+
ctx.signal?.throwIfAborted();
|
|
53
|
+
for (const [index, filename] of filenames.entries()) {
|
|
54
|
+
const written = await engine.files.writeFromPath(assetId, filename, outputs[index]);
|
|
55
|
+
if (!written.ok)
|
|
56
|
+
throw new Error(written.error.message);
|
|
57
|
+
}
|
|
58
|
+
const recorded = await engine.history.recordOperation("even_split", assetId, {
|
|
59
|
+
source_asset_id: payload.sourceAssetId,
|
|
60
|
+
filenames,
|
|
61
|
+
chunk_count: payload.chunks.length,
|
|
62
|
+
split_at_iso: new Date().toISOString(),
|
|
63
|
+
});
|
|
64
|
+
if (!recorded.ok)
|
|
65
|
+
throw new Error(recorded.error.message);
|
|
66
|
+
return { success: true, destination_asset_id: assetId, filenames };
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await rm(workspace, { recursive: true, force: true });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export function registerEvenSplitJob(engine) {
|
|
74
|
+
registerJobHandler("even_split", async (payload, ctx) => {
|
|
75
|
+
const { sourcePath, sourceAssetId, chunks, bookId, destination } = payload;
|
|
76
|
+
if (destination)
|
|
77
|
+
return splitIntoDestination(engine, payload, ctx);
|
|
78
|
+
const splitAtIso = new Date().toISOString();
|
|
79
|
+
const buildPromises = chunks.map((chunk, i) => {
|
|
80
|
+
const provenance = {
|
|
81
|
+
origin: "even_split",
|
|
82
|
+
source_asset_id: sourceAssetId,
|
|
83
|
+
chunk_index: i,
|
|
84
|
+
chunk_count: chunks.length,
|
|
85
|
+
chunk_start: chunk.start,
|
|
86
|
+
chunk_end: chunk.end,
|
|
87
|
+
chunk_duration: chunk.duration,
|
|
88
|
+
split_at_iso: splitAtIso,
|
|
89
|
+
};
|
|
90
|
+
// Targeted chunks need exact-second container duration; the trailing
|
|
91
|
+
// leftover does not (and the user explicitly doesn't care).
|
|
92
|
+
return buildChunk(engine, {
|
|
93
|
+
sourcePath,
|
|
94
|
+
outputPath: chunk.outputPath,
|
|
95
|
+
start: chunk.start,
|
|
96
|
+
end: chunk.end,
|
|
97
|
+
assetId: chunk.assetId,
|
|
98
|
+
provenance,
|
|
99
|
+
exact: chunk.exact,
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
await Promise.all(buildPromises);
|
|
103
|
+
await Promise.all(chunks.map((chunk, i) => queue.enqueueAndWait(`process-video-${chunk.assetId}-${Date.now() + i}`, {
|
|
104
|
+
type: "process_video",
|
|
105
|
+
videoPath: chunk.outputPath,
|
|
106
|
+
assetId: chunk.assetId,
|
|
107
|
+
assetDir: chunk.assetDir,
|
|
108
|
+
bookId,
|
|
109
|
+
skipDownstreamJobs: true,
|
|
110
|
+
provenance: {
|
|
111
|
+
origin: "even_split",
|
|
112
|
+
source_asset_id: sourceAssetId,
|
|
113
|
+
chunk_index: i,
|
|
114
|
+
chunk_count: chunks.length,
|
|
115
|
+
},
|
|
116
|
+
})));
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
chunk_asset_ids: chunks.map((c) => c.assetId),
|
|
120
|
+
};
|
|
121
|
+
});
|
|
122
|
+
}
|