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,200 @@
|
|
|
1
|
+
import { patchPostRuntime, readPostRuntime } from "./posts/post-store.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { logger } from "./logger.js";
|
|
4
|
+
import { listBookEngines, queue, resolveBookEngine, } from "./queue.js";
|
|
5
|
+
const REARM_INTERVAL_MS = 12 * 60 * 60_000;
|
|
6
|
+
const SAFE_TIMEOUT_MS = 2_147_483_000;
|
|
7
|
+
export const SCHEDULED_POST_SETTINGS_KEY = "application.scheduled_posts.v1";
|
|
8
|
+
const armed = new Map();
|
|
9
|
+
let writeChain = Promise.resolve();
|
|
10
|
+
let rearmInterval = null;
|
|
11
|
+
function serialize(fn) {
|
|
12
|
+
const next = writeChain.then(fn, fn);
|
|
13
|
+
writeChain = next.catch(() => { });
|
|
14
|
+
return next;
|
|
15
|
+
}
|
|
16
|
+
function readState(engine) {
|
|
17
|
+
const entries = engine.settings.get(SCHEDULED_POST_SETTINGS_KEY);
|
|
18
|
+
return Array.isArray(entries) ? entries : [];
|
|
19
|
+
}
|
|
20
|
+
function writeState(engine, entries) {
|
|
21
|
+
const result = engine.settings.set(SCHEDULED_POST_SETTINGS_KEY, entries);
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
throw new Error(result.error.message);
|
|
24
|
+
}
|
|
25
|
+
async function updateEntry(bookId, scheduleId, patch) {
|
|
26
|
+
await serialize(async () => {
|
|
27
|
+
const engine = await resolveBookEngine(bookId);
|
|
28
|
+
const entries = readState(engine);
|
|
29
|
+
const updated = entries.map((entry) => entry.schedule_id === scheduleId ? { ...entry, ...patch } : entry);
|
|
30
|
+
writeState(engine, updated);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function timerKey(bookId, scheduleId) {
|
|
34
|
+
return `${bookId}:${scheduleId}`;
|
|
35
|
+
}
|
|
36
|
+
function armTimer(bookId, entry) {
|
|
37
|
+
const key = timerKey(bookId, entry.schedule_id);
|
|
38
|
+
if (armed.has(key))
|
|
39
|
+
return;
|
|
40
|
+
if (entry.status !== "pending")
|
|
41
|
+
return;
|
|
42
|
+
const delay = new Date(entry.scheduled_for).getTime() - Date.now();
|
|
43
|
+
const safeDelay = Math.min(Math.max(delay, 0), SAFE_TIMEOUT_MS);
|
|
44
|
+
if (delay > SAFE_TIMEOUT_MS) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const t = setTimeout(() => {
|
|
48
|
+
armed.delete(key);
|
|
49
|
+
void firePost(bookId, entry.schedule_id).catch((e) => logger.error("scheduled-post-failed", { bookId, scheduleId: entry.schedule_id, error: e }));
|
|
50
|
+
}, safeDelay);
|
|
51
|
+
armed.set(key, t);
|
|
52
|
+
}
|
|
53
|
+
async function firePost(bookId, scheduleId) {
|
|
54
|
+
const engine = await resolveBookEngine(bookId);
|
|
55
|
+
const entries = readState(engine);
|
|
56
|
+
const entry = entries.find((e) => e.schedule_id === scheduleId);
|
|
57
|
+
if (!entry || entry.status !== "pending")
|
|
58
|
+
return;
|
|
59
|
+
const jobId = `post-x-${scheduleId}`;
|
|
60
|
+
try {
|
|
61
|
+
const result = await queue.enqueueAndWait(jobId, {
|
|
62
|
+
type: "post_to_x",
|
|
63
|
+
assetId: entry.payload.asset_id,
|
|
64
|
+
postCellId: entry.payload.post_cell_id,
|
|
65
|
+
bookId: entry.payload.book_id,
|
|
66
|
+
text: entry.payload.text,
|
|
67
|
+
orientation: entry.payload.orientation,
|
|
68
|
+
scheduleId,
|
|
69
|
+
scheduledFor: entry.scheduled_for,
|
|
70
|
+
followups: entry.payload.followups,
|
|
71
|
+
});
|
|
72
|
+
await updateEntry(bookId, scheduleId, {
|
|
73
|
+
status: result.status,
|
|
74
|
+
tweet_id: result.tweet_id,
|
|
75
|
+
tweet_url: result.tweet_url,
|
|
76
|
+
replies: result.replies,
|
|
77
|
+
followup_error: result.followup_error,
|
|
78
|
+
...(result.followup_error ? { error: result.followup_error.message } : {}),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
catch (err) {
|
|
82
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
83
|
+
await updateEntry(bookId, scheduleId, { status: "failed", error: message });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export async function schedulePost(payload, scheduledFor) {
|
|
87
|
+
const when = new Date(scheduledFor);
|
|
88
|
+
if (Number.isNaN(when.getTime())) {
|
|
89
|
+
throw new Error(`Invalid scheduled_for: ${scheduledFor}`);
|
|
90
|
+
}
|
|
91
|
+
const engine = await resolveBookEngine(payload.book_id);
|
|
92
|
+
if (payload.post_cell_id) {
|
|
93
|
+
const current = readPostRuntime(engine, payload.post_cell_id);
|
|
94
|
+
if (current.url || ["posting", "needs_reconciliation"].includes(current.status))
|
|
95
|
+
throw new Error("This Post is already published or needs reconciliation");
|
|
96
|
+
if (current.status === "scheduled" && current.scheduleId) {
|
|
97
|
+
if (current.scheduledFor === when.toISOString())
|
|
98
|
+
return { schedule_id: current.scheduleId, scheduled_for: current.scheduledFor };
|
|
99
|
+
await cancelScheduledPost(current.scheduleId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const entry = {
|
|
103
|
+
schedule_id: randomUUID(),
|
|
104
|
+
payload,
|
|
105
|
+
scheduled_for: when.toISOString(),
|
|
106
|
+
created_at: new Date().toISOString(),
|
|
107
|
+
status: "pending",
|
|
108
|
+
};
|
|
109
|
+
await serialize(async () => {
|
|
110
|
+
const entries = readState(engine);
|
|
111
|
+
entries.push(entry);
|
|
112
|
+
writeState(engine, entries);
|
|
113
|
+
});
|
|
114
|
+
if (payload.post_cell_id)
|
|
115
|
+
patchPostRuntime(engine, payload.post_cell_id, { status: "scheduled", scheduledFor: entry.scheduled_for, scheduleId: entry.schedule_id });
|
|
116
|
+
armTimer(payload.book_id, entry);
|
|
117
|
+
return { schedule_id: entry.schedule_id, scheduled_for: entry.scheduled_for };
|
|
118
|
+
}
|
|
119
|
+
export async function cancelScheduledPost(scheduleId) {
|
|
120
|
+
let found = false;
|
|
121
|
+
const engines = await listBookEngines();
|
|
122
|
+
await serialize(async () => {
|
|
123
|
+
for (const engine of engines) {
|
|
124
|
+
const bookId = engine.book.get().bookId;
|
|
125
|
+
const entries = readState(engine);
|
|
126
|
+
let changed = false;
|
|
127
|
+
const updated = entries.map((entry) => {
|
|
128
|
+
if (entry.schedule_id !== scheduleId || entry.status !== "pending") {
|
|
129
|
+
return entry;
|
|
130
|
+
}
|
|
131
|
+
found = true;
|
|
132
|
+
if (entry.payload.post_cell_id)
|
|
133
|
+
patchPostRuntime(engine, entry.payload.post_cell_id, { status: "draft", scheduledFor: undefined, scheduleId: undefined });
|
|
134
|
+
changed = true;
|
|
135
|
+
return { ...entry, status: "cancelled" };
|
|
136
|
+
});
|
|
137
|
+
if (!changed)
|
|
138
|
+
continue;
|
|
139
|
+
const key = timerKey(bookId, scheduleId);
|
|
140
|
+
const timer = armed.get(key);
|
|
141
|
+
if (timer)
|
|
142
|
+
clearTimeout(timer);
|
|
143
|
+
armed.delete(key);
|
|
144
|
+
writeState(engine, updated);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
return found;
|
|
148
|
+
}
|
|
149
|
+
export async function listScheduledPosts() {
|
|
150
|
+
const engines = await listBookEngines();
|
|
151
|
+
return engines
|
|
152
|
+
.flatMap((engine) => readState(engine))
|
|
153
|
+
.sort((left, right) => left.created_at.localeCompare(right.created_at));
|
|
154
|
+
}
|
|
155
|
+
export async function loadAndArmAll() {
|
|
156
|
+
const now = Date.now();
|
|
157
|
+
const engines = await listBookEngines();
|
|
158
|
+
for (const engine of engines) {
|
|
159
|
+
const bookId = engine.book.get().bookId;
|
|
160
|
+
for (const entry of readState(engine)) {
|
|
161
|
+
if (entry.status !== "pending")
|
|
162
|
+
continue;
|
|
163
|
+
const due = new Date(entry.scheduled_for).getTime();
|
|
164
|
+
if (due <= now) {
|
|
165
|
+
void firePost(bookId, entry.schedule_id).catch((e) => logger.error("scheduled-post-failed", { bookId, scheduleId: entry.schedule_id, error: e }));
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
armTimer(bookId, entry);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (!rearmInterval) {
|
|
173
|
+
rearmInterval = setInterval(() => {
|
|
174
|
+
void rearmPending().catch((e) => logger.error("scheduled-post-rearm-failed", { error: e }));
|
|
175
|
+
}, REARM_INTERVAL_MS);
|
|
176
|
+
rearmInterval.unref?.();
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function rearmPending() {
|
|
180
|
+
const engines = await listBookEngines();
|
|
181
|
+
for (const engine of engines) {
|
|
182
|
+
const bookId = engine.book.get().bookId;
|
|
183
|
+
for (const entry of readState(engine)) {
|
|
184
|
+
if (entry.status !== "pending")
|
|
185
|
+
continue;
|
|
186
|
+
if (armed.has(timerKey(bookId, entry.schedule_id)))
|
|
187
|
+
continue;
|
|
188
|
+
armTimer(bookId, entry);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export function stopScheduler() {
|
|
193
|
+
for (const t of armed.values())
|
|
194
|
+
clearTimeout(t);
|
|
195
|
+
armed.clear();
|
|
196
|
+
if (rearmInterval) {
|
|
197
|
+
clearInterval(rearmInterval);
|
|
198
|
+
rearmInterval = null;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { LOCAL_CLIP_MANIFEST } from "videobook-engine";
|
|
2
|
+
export function missingVisualIndexes(engine, assets) {
|
|
3
|
+
return assets.filter((asset) => {
|
|
4
|
+
if (asset.status !== "ready" || !["image", "video", "final"].includes(asset.type))
|
|
5
|
+
return false;
|
|
6
|
+
if (!asset.object_hash)
|
|
7
|
+
return true;
|
|
8
|
+
const plan = engine.temporalSearch.plan(asset.artifact_id, asset.object_hash, LOCAL_CLIP_MANIFEST.manifestId, LOCAL_CLIP_MANIFEST.manifestId);
|
|
9
|
+
if (!plan.ok)
|
|
10
|
+
return true;
|
|
11
|
+
return !plan.value.coverage.some((entry) => entry.phase === "visual" && entry.state === "ready" && entry.indexedUnits > 0);
|
|
12
|
+
});
|
|
13
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { findVideoFile } from "videobook-engine";
|
|
5
|
+
import { probeVideo, runFfmpeg } from "./ffmpeg.js";
|
|
6
|
+
import { formatTimestamp, generateTimestamps } from "./frame-utils.js";
|
|
7
|
+
import { registerMediaStreams } from "./jobs/process-video.js";
|
|
8
|
+
async function prepareSearchFrames(engine, artifactId, signal) {
|
|
9
|
+
const streams = engine.streams.list(artifactId);
|
|
10
|
+
if (!streams.ok)
|
|
11
|
+
throw new Error(streams.error.message);
|
|
12
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
13
|
+
if (!manifest.ok)
|
|
14
|
+
throw Object.assign(new Error(manifest.error.message), { name: manifest.error.code, error: manifest.error });
|
|
15
|
+
const video = streams.value.find((stream) => stream.kind === "video"
|
|
16
|
+
&& manifest.value.files.some((file) => file.name === stream.sourcePath && file.objectHash === stream.objectHash));
|
|
17
|
+
if (!video)
|
|
18
|
+
return;
|
|
19
|
+
const marker = await engine.metadata.artifacts.read(artifactId, "migrationFrames");
|
|
20
|
+
if (!marker.ok && marker.error.code !== "NOT_FOUND")
|
|
21
|
+
throw new Error(marker.error.message);
|
|
22
|
+
if (marker.ok && marker.value.objectHash !== video.objectHash)
|
|
23
|
+
throw new Error("Video changed before its search frames were prepared");
|
|
24
|
+
if (!marker.ok && manifest.value.files.some((file) => /^original_frames\/\d+(?:\.\d+)?\.jpg$/u.test(file.name)))
|
|
25
|
+
return;
|
|
26
|
+
const duration = video.durationTicks * video.timeBase.numerator / video.timeBase.denominator;
|
|
27
|
+
const frames = marker.ok ? marker.value.frames : generateTimestamps(duration).map((time) => `original_frames/${formatTimestamp(time)}.jpg`);
|
|
28
|
+
if (!marker.ok) {
|
|
29
|
+
const written = await engine.metadata.artifacts.write(artifactId, "migrationFrames", { objectHash: video.objectHash, frames });
|
|
30
|
+
if (!written.ok)
|
|
31
|
+
throw new Error(written.error.message);
|
|
32
|
+
}
|
|
33
|
+
const existing = new Set(manifest.value.files.map((file) => file.name));
|
|
34
|
+
const temporary = await mkdtemp(join(tmpdir(), "videobook-search-frames-"));
|
|
35
|
+
try {
|
|
36
|
+
for (const name of frames) {
|
|
37
|
+
signal?.throwIfAborted();
|
|
38
|
+
if (!/^original_frames\/\d+(?:\.\d+)?\.jpg$/u.test(name))
|
|
39
|
+
throw new Error("Invalid search frame record");
|
|
40
|
+
if (existing.has(name))
|
|
41
|
+
continue;
|
|
42
|
+
const target = join(temporary, "frame.jpg");
|
|
43
|
+
await runFfmpeg(["-y", "-ss", name.slice("original_frames/".length, -4),
|
|
44
|
+
"-i", join(manifest.value.path, video.sourcePath), "-map", `0:${video.streamIndex}`, "-frames:v", "1", "-q:v", "2", target], { signal });
|
|
45
|
+
const written = await engine.files.write(artifactId, name, await readFile(target));
|
|
46
|
+
if (!written.ok)
|
|
47
|
+
throw new Error(written.error.message);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
finally {
|
|
51
|
+
await rm(temporary, { recursive: true, force: true, maxRetries: 3 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export async function prepareSearchVideo(engine, artifactId, signal) {
|
|
55
|
+
const manifest = await engine.files.manifest(artifactId);
|
|
56
|
+
if (!manifest.ok)
|
|
57
|
+
throw new Error(manifest.error.message);
|
|
58
|
+
const file = findVideoFile(manifest.value.files);
|
|
59
|
+
if (!file)
|
|
60
|
+
throw new Error("No video source is available for indexing");
|
|
61
|
+
const streams = engine.streams.list(artifactId);
|
|
62
|
+
if (!streams.ok)
|
|
63
|
+
throw new Error(streams.error.message);
|
|
64
|
+
if (!streams.value.some((stream) => stream.kind === "video" && stream.objectHash === file.objectHash)) {
|
|
65
|
+
const probe = await probeVideo(join(manifest.value.path, file.name), { signal });
|
|
66
|
+
if (!probe.videoStream)
|
|
67
|
+
throw new Error("No usable video stream is available for indexing");
|
|
68
|
+
await registerMediaStreams(engine, artifactId, file.name, probe);
|
|
69
|
+
}
|
|
70
|
+
await prepareSearchFrames(engine, artifactId, signal);
|
|
71
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { v7 as uuidv7 } from "uuid";
|
|
2
|
+
export class SearchReferenceStore {
|
|
3
|
+
entries = new Map();
|
|
4
|
+
ttlMs;
|
|
5
|
+
maxEntries;
|
|
6
|
+
now;
|
|
7
|
+
constructor(ttlMs = 30 * 60_000, maxEntries = 128, now = Date.now) {
|
|
8
|
+
this.ttlMs = ttlMs;
|
|
9
|
+
this.maxEntries = maxEntries;
|
|
10
|
+
this.now = now;
|
|
11
|
+
}
|
|
12
|
+
put(input) {
|
|
13
|
+
this.purge();
|
|
14
|
+
while (this.entries.size >= this.maxEntries) {
|
|
15
|
+
const oldest = this.entries.keys().next().value;
|
|
16
|
+
if (typeof oldest !== "string")
|
|
17
|
+
break;
|
|
18
|
+
this.entries.delete(oldest);
|
|
19
|
+
}
|
|
20
|
+
const referenceId = uuidv7();
|
|
21
|
+
const descriptor = {
|
|
22
|
+
reference_id: referenceId,
|
|
23
|
+
kind: input.kind,
|
|
24
|
+
name: input.name,
|
|
25
|
+
...(input.durationMs === undefined
|
|
26
|
+
? {}
|
|
27
|
+
: { duration_ms: input.durationMs }),
|
|
28
|
+
expires_at: this.now() + this.ttlMs,
|
|
29
|
+
...(input.preview ? { preview_url: `/api/search-references/${referenceId}/preview` } : {}),
|
|
30
|
+
};
|
|
31
|
+
this.entries.set(referenceId, {
|
|
32
|
+
descriptor,
|
|
33
|
+
reference: input.reference,
|
|
34
|
+
...(input.preview ? { preview: input.preview } : {}),
|
|
35
|
+
});
|
|
36
|
+
return descriptor;
|
|
37
|
+
}
|
|
38
|
+
get(referenceId) {
|
|
39
|
+
this.purge();
|
|
40
|
+
const entry = this.entries.get(referenceId);
|
|
41
|
+
if (!entry) {
|
|
42
|
+
throw new Error("Search reference expired or was released");
|
|
43
|
+
}
|
|
44
|
+
this.entries.delete(referenceId);
|
|
45
|
+
this.entries.set(referenceId, entry);
|
|
46
|
+
return entry;
|
|
47
|
+
}
|
|
48
|
+
release(referenceId) {
|
|
49
|
+
return this.entries.delete(referenceId);
|
|
50
|
+
}
|
|
51
|
+
purge() {
|
|
52
|
+
const now = this.now();
|
|
53
|
+
for (const [referenceId, entry] of this.entries) {
|
|
54
|
+
if (entry.descriptor.expires_at <= now) {
|
|
55
|
+
this.entries.delete(referenceId);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export const searchReferenceStore = new SearchReferenceStore();
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Engine hit scores are reciprocal-rank values, so they only order hits within
|
|
2
|
+
// a single book. Ranking across books must use the per-signal similarity
|
|
3
|
+
// scores, which are comparable because every book shares the same embedding
|
|
4
|
+
// spaces; exact and near fingerprint matches outrank any similarity.
|
|
5
|
+
export function comparableHitScore(hit) {
|
|
6
|
+
let boost = 0;
|
|
7
|
+
let strongest = 0;
|
|
8
|
+
for (const signal of hit.signals) {
|
|
9
|
+
if (signal.kind === "exact")
|
|
10
|
+
boost = Math.max(boost, 1);
|
|
11
|
+
else if (signal.kind === "near")
|
|
12
|
+
boost = Math.max(boost, 0.25);
|
|
13
|
+
else
|
|
14
|
+
strongest = Math.max(strongest, Math.min(signal.score ?? 0, 1));
|
|
15
|
+
}
|
|
16
|
+
return Math.min(boost + strongest, 1);
|
|
17
|
+
}
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { once } from "node:events";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { stopAceServer } from "./acestep/process.js";
|
|
4
|
+
import { createApp } from "./app.js";
|
|
5
|
+
import { bookManager, scopedEngine } from "./engine.js";
|
|
6
|
+
import { getDefaultSecretStore } from "./env-secrets.js";
|
|
7
|
+
import { hydrateLibrary } from "./hydration.js";
|
|
8
|
+
import { registerAllJobHandlers } from "./jobs/index.js";
|
|
9
|
+
import { logger } from "./logger.js";
|
|
10
|
+
import { stopPostMetricsScheduler } from "./posts/post-metrics.js";
|
|
11
|
+
import { queue } from "./queue.js";
|
|
12
|
+
import { stopScheduler } from "./scheduled-posts.js";
|
|
13
|
+
import { SERVER_VERSION } from "./server.js";
|
|
14
|
+
import { ServerAlreadyRunningError, acquireServerLock, inspectServerOwner, } from "./server-lock.js";
|
|
15
|
+
import { attachVoiceAudio } from "./voice-audio.js";
|
|
16
|
+
const LOOPBACK_HOST = "127.0.0.1";
|
|
17
|
+
const SHUTDOWN_WATCHDOG_MS = 20_000;
|
|
18
|
+
const HTTP_CLOSE_GRACE_MS = 2_000;
|
|
19
|
+
async function shutdownServices(services) {
|
|
20
|
+
stopScheduler();
|
|
21
|
+
stopPostMetricsScheduler();
|
|
22
|
+
await services.voiceSessions.close();
|
|
23
|
+
await services.mcpHandler.close();
|
|
24
|
+
await queue.stop();
|
|
25
|
+
await stopAceServer();
|
|
26
|
+
await bookManager.close();
|
|
27
|
+
}
|
|
28
|
+
async function finishHttpClose(server, closed) {
|
|
29
|
+
server.closeAllConnections();
|
|
30
|
+
await Promise.race([closed, delay(HTTP_CLOSE_GRACE_MS, undefined, { ref: false })]);
|
|
31
|
+
}
|
|
32
|
+
function closer(server, services, lock, onClosing) {
|
|
33
|
+
let closing;
|
|
34
|
+
async function run() {
|
|
35
|
+
onClosing();
|
|
36
|
+
const closed = new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
37
|
+
try {
|
|
38
|
+
await shutdownServices(services);
|
|
39
|
+
logger.info("shutdown-complete");
|
|
40
|
+
}
|
|
41
|
+
catch (error) {
|
|
42
|
+
logger.error("shutdown-failed", { error });
|
|
43
|
+
throw error;
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await finishHttpClose(server, closed);
|
|
47
|
+
await lock.release();
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return () => {
|
|
51
|
+
closing ??= run();
|
|
52
|
+
return closing;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
function boundPort(server, fallback) {
|
|
56
|
+
const address = server.address();
|
|
57
|
+
return address && typeof address !== "string" ? address.port : fallback;
|
|
58
|
+
}
|
|
59
|
+
function startHydration(isClosing) {
|
|
60
|
+
const ready = hydrateLibrary(bookManager, {
|
|
61
|
+
devFixtures: process.env.VIDEOBOOK_DEV_FIXTURES === "1",
|
|
62
|
+
}).catch((error) => {
|
|
63
|
+
if (!isClosing())
|
|
64
|
+
throw error;
|
|
65
|
+
});
|
|
66
|
+
ready.catch(() => undefined);
|
|
67
|
+
return ready;
|
|
68
|
+
}
|
|
69
|
+
async function refuseRunningOwner(libraryDir) {
|
|
70
|
+
const owner = await inspectServerOwner(libraryDir);
|
|
71
|
+
if (owner.kind === "none")
|
|
72
|
+
return;
|
|
73
|
+
throw new ServerAlreadyRunningError(owner.record, libraryDir, owner.kind === "running");
|
|
74
|
+
}
|
|
75
|
+
async function claimLibrary(server, libraryDir, port, url) {
|
|
76
|
+
try {
|
|
77
|
+
return await acquireServerLock(libraryDir, { pid: process.pid, port, url, version: SERVER_VERSION });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
await new Promise((resolveClose) => server.close(() => resolveClose()));
|
|
81
|
+
throw error;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
export async function startServer(options) {
|
|
85
|
+
const libraryDir = bookManager.rootDir;
|
|
86
|
+
await refuseRunningOwner(libraryDir);
|
|
87
|
+
getDefaultSecretStore().resolveOnePassword();
|
|
88
|
+
registerAllJobHandlers(scopedEngine);
|
|
89
|
+
const services = createApp({ ui: options.ui, uiDir: options.uiDir });
|
|
90
|
+
const server = services.app.listen(options.port, LOOPBACK_HOST);
|
|
91
|
+
await once(server, "listening");
|
|
92
|
+
const port = boundPort(server, options.port);
|
|
93
|
+
const url = `http://${LOOPBACK_HOST}:${port}`;
|
|
94
|
+
const lock = await claimLibrary(server, libraryDir, port, url);
|
|
95
|
+
logger.info("server-listening", { port });
|
|
96
|
+
attachVoiceAudio(server, services.voiceSessions);
|
|
97
|
+
let closing = false;
|
|
98
|
+
const close = closer(server, services, lock, () => {
|
|
99
|
+
closing = true;
|
|
100
|
+
});
|
|
101
|
+
return {
|
|
102
|
+
port,
|
|
103
|
+
url,
|
|
104
|
+
mcpUrl: `${url}/mcp`,
|
|
105
|
+
libraryDir,
|
|
106
|
+
uiDir: services.uiDir,
|
|
107
|
+
uiStatus: services.uiStatus,
|
|
108
|
+
ready: startHydration(() => closing),
|
|
109
|
+
close,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
export function installSignalHandlers(starting, exit = (code) => process.exit(code)) {
|
|
113
|
+
let stopping = false;
|
|
114
|
+
function stop() {
|
|
115
|
+
if (stopping)
|
|
116
|
+
return;
|
|
117
|
+
stopping = true;
|
|
118
|
+
setTimeout(() => exit(1), SHUTDOWN_WATCHDOG_MS).unref();
|
|
119
|
+
starting
|
|
120
|
+
.then((running) => running.close())
|
|
121
|
+
.then(() => exit(0), () => exit(1));
|
|
122
|
+
}
|
|
123
|
+
process.on("SIGINT", stop);
|
|
124
|
+
process.on("SIGTERM", stop);
|
|
125
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { link, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
export const SERVER_LOCK_FILENAME = ".videobook-server.json";
|
|
5
|
+
const HEALTH_TIMEOUT_MS = 1_500;
|
|
6
|
+
export class ServerAlreadyRunningError extends Error {
|
|
7
|
+
running;
|
|
8
|
+
libraryDir;
|
|
9
|
+
constructor(running, libraryDir, responding) {
|
|
10
|
+
super(responding
|
|
11
|
+
? `videobook is already running at ${running.url} (pid ${running.pid}) for library ${libraryDir}. Open that URL or stop it first.`
|
|
12
|
+
: `videobook (pid ${running.pid}) holds library ${libraryDir} at ${running.url} but is not responding. Stop that process first.`);
|
|
13
|
+
this.name = "ServerAlreadyRunningError";
|
|
14
|
+
this.running = running;
|
|
15
|
+
this.libraryDir = libraryDir;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function serverLockPath(libraryDir) {
|
|
19
|
+
return join(libraryDir, SERVER_LOCK_FILENAME);
|
|
20
|
+
}
|
|
21
|
+
export function processAlive(pid) {
|
|
22
|
+
try {
|
|
23
|
+
process.kill(pid, 0);
|
|
24
|
+
return true;
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
return error.code === "EPERM";
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function errorCode(error) {
|
|
31
|
+
return error?.code;
|
|
32
|
+
}
|
|
33
|
+
function parseServerLock(raw) {
|
|
34
|
+
try {
|
|
35
|
+
const value = JSON.parse(raw);
|
|
36
|
+
if (typeof value.pid !== "number" || typeof value.port !== "number" || typeof value.url !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
return {
|
|
39
|
+
pid: value.pid,
|
|
40
|
+
port: value.port,
|
|
41
|
+
url: value.url,
|
|
42
|
+
startedAt: typeof value.startedAt === "string" ? value.startedAt : "",
|
|
43
|
+
version: typeof value.version === "string" ? value.version : "",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export async function readServerLock(libraryDir) {
|
|
51
|
+
try {
|
|
52
|
+
return parseServerLock(await readFile(serverLockPath(libraryDir), "utf8"));
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (errorCode(error) === "ENOENT")
|
|
56
|
+
return null;
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function healthFailure(error) {
|
|
61
|
+
if (error instanceof Error && (error.name === "TimeoutError" || error.name === "AbortError"))
|
|
62
|
+
return "timeout";
|
|
63
|
+
return "refused";
|
|
64
|
+
}
|
|
65
|
+
async function askHealth(url, timeoutMs) {
|
|
66
|
+
try {
|
|
67
|
+
const response = await fetch(new URL("/healthz", url), { signal: AbortSignal.timeout(timeoutMs) });
|
|
68
|
+
if (!response.ok)
|
|
69
|
+
return "other";
|
|
70
|
+
const body = await response.json();
|
|
71
|
+
return body.ok === true && body.service === "videobook" ? "videobook" : "other";
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
return healthFailure(error);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export async function inspectServerOwner(libraryDir, timeoutMs = HEALTH_TIMEOUT_MS) {
|
|
78
|
+
const record = await readServerLock(libraryDir);
|
|
79
|
+
if (!record || !processAlive(record.pid))
|
|
80
|
+
return { kind: "none" };
|
|
81
|
+
const answer = await askHealth(record.url, timeoutMs);
|
|
82
|
+
if (answer === "videobook")
|
|
83
|
+
return { kind: "running", record };
|
|
84
|
+
if (answer === "timeout")
|
|
85
|
+
return { kind: "unresponsive", record };
|
|
86
|
+
return { kind: "none" };
|
|
87
|
+
}
|
|
88
|
+
function sameRecord(left, right) {
|
|
89
|
+
return left !== null && left.pid === right.pid && left.startedAt === right.startedAt;
|
|
90
|
+
}
|
|
91
|
+
async function writeExclusive(path, record) {
|
|
92
|
+
const staging = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
93
|
+
await writeFile(staging, `${JSON.stringify(record)}\n`, { encoding: "utf8", flag: "wx" });
|
|
94
|
+
try {
|
|
95
|
+
await link(staging, path);
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (errorCode(error) === "EEXIST")
|
|
100
|
+
return false;
|
|
101
|
+
throw error;
|
|
102
|
+
}
|
|
103
|
+
finally {
|
|
104
|
+
await unlink(staging).catch(() => undefined);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async function removeIfUnchanged(libraryDir, expected) {
|
|
108
|
+
const current = await readServerLock(libraryDir);
|
|
109
|
+
if (expected === null ? current !== null : !sameRecord(current, expected))
|
|
110
|
+
return;
|
|
111
|
+
await unlink(serverLockPath(libraryDir)).catch((error) => {
|
|
112
|
+
if (errorCode(error) !== "ENOENT")
|
|
113
|
+
throw error;
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
function lockHandle(libraryDir, record) {
|
|
117
|
+
let released = false;
|
|
118
|
+
return {
|
|
119
|
+
path: serverLockPath(libraryDir),
|
|
120
|
+
record,
|
|
121
|
+
async release() {
|
|
122
|
+
if (released)
|
|
123
|
+
return;
|
|
124
|
+
released = true;
|
|
125
|
+
await removeIfUnchanged(libraryDir, record);
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function acquireServerLock(libraryDir, fields) {
|
|
130
|
+
await mkdir(libraryDir, { recursive: true });
|
|
131
|
+
const record = { ...fields, startedAt: new Date().toISOString() };
|
|
132
|
+
const path = serverLockPath(libraryDir);
|
|
133
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
134
|
+
if (await writeExclusive(path, record))
|
|
135
|
+
return lockHandle(libraryDir, record);
|
|
136
|
+
const stale = await readServerLock(libraryDir);
|
|
137
|
+
const owner = await inspectServerOwner(libraryDir);
|
|
138
|
+
if (owner.kind !== "none")
|
|
139
|
+
throw new ServerAlreadyRunningError(owner.record, libraryDir, owner.kind === "running");
|
|
140
|
+
await removeIfUnchanged(libraryDir, stale);
|
|
141
|
+
}
|
|
142
|
+
throw new Error(`Could not acquire the videobook server lock at ${path}`);
|
|
143
|
+
}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { createMcpHandler, McpServer, } from "@modelcontextprotocol/server";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { registerAllTools } from "./tools/index.js";
|
|
4
|
+
import { bookManager, scopedEngine as defaultFs } from "./engine.js";
|
|
5
|
+
import { allowsHttpRequest } from "./http-access.js";
|
|
6
|
+
export const MCP_PROTOCOL_VERSION = "2026-07-28";
|
|
7
|
+
export const SERVER_VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
8
|
+
export function serverInstructions(baseUrl) {
|
|
9
|
+
return [
|
|
10
|
+
"Videobook is a local-first notebook for AI video creation.",
|
|
11
|
+
`Tools return media as paths such as /files/<bookId>/<artifactId>/<name>; fetch them from ${baseUrl}.`,
|
|
12
|
+
`The web UI for this library is at ${baseUrl}/.`,
|
|
13
|
+
"Generation and rendering run as queued jobs; follow them with list_jobs or wait_for_asset_ready.",
|
|
14
|
+
].join(" ");
|
|
15
|
+
}
|
|
16
|
+
export function createServer(engine = defaultFs, books = bookManager, options = {}) {
|
|
17
|
+
const server = new McpServer({
|
|
18
|
+
name: "videobook",
|
|
19
|
+
version: SERVER_VERSION,
|
|
20
|
+
}, {
|
|
21
|
+
supportedProtocolVersions: [MCP_PROTOCOL_VERSION],
|
|
22
|
+
...(options.instructions ? { instructions: options.instructions } : {}),
|
|
23
|
+
});
|
|
24
|
+
registerAllTools(server, engine, books);
|
|
25
|
+
return server;
|
|
26
|
+
}
|
|
27
|
+
export function createHttpHandler(engine = defaultFs, books = bookManager) {
|
|
28
|
+
const resolveEngine = typeof engine === "function" ? engine : () => engine;
|
|
29
|
+
const handler = createMcpHandler(() => createServer(resolveEngine(), books), {
|
|
30
|
+
legacy: "reject",
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
...handler,
|
|
34
|
+
async fetch(request, options) {
|
|
35
|
+
const urlHost = new URL(request.url).host;
|
|
36
|
+
if (!allowsHttpRequest(urlHost, null, null) || !allowsHttpRequest(request.headers.get("host") ?? urlHost, request.headers.get("origin"), request.headers.get("sec-fetch-site"))) {
|
|
37
|
+
return Response.json({ error: "HTTP request origin or host is not allowed" }, { status: 403 });
|
|
38
|
+
}
|
|
39
|
+
return handler.fetch(request, options);
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|