videobook 0.0.0-stage → 0.1.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 +18 -0
- package/LICENSE +21 -0
- package/README.md +133 -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 +192 -0
- package/dist/cli/doctor-command.js +255 -0
- package/dist/cli/entry.js +2 -0
- package/dist/cli/main.js +91 -0
- package/dist/cli/mcp-command.js +82 -0
- package/dist/cli/mcp-relay.js +51 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/run-command.js +187 -0
- package/dist/cli/serve-command.js +57 -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 +446 -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-secrets.js +184 -0
- package/dist/ffmpeg-binary.js +12 -0
- package/dist/ffmpeg.js +425 -0
- package/dist/ffprobe-binary.js +55 -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/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 +457 -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 +426 -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 +9 -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/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 +806 -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 +150 -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 +2006 -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/zod-slots.js +334 -0
- package/package.json +92 -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,99 @@
|
|
|
1
|
+
import { createOpenAi } from "./usage-providers.js";
|
|
2
|
+
import { withRetry } from "@apicity/openai";
|
|
3
|
+
export const INTELLIGENCE_MODEL = "gpt-5.6-sol";
|
|
4
|
+
export const INTELLIGENCE_REASONING_EFFORT = "medium";
|
|
5
|
+
export const OPENAI_ANALYSIS_TOOL = "openai_responses_analysis";
|
|
6
|
+
export const ANALYSIS_REASONING_EFFORT = "high";
|
|
7
|
+
export function intelligenceRequest(request, options = {}) {
|
|
8
|
+
return {
|
|
9
|
+
...request,
|
|
10
|
+
model: options.model ?? INTELLIGENCE_MODEL,
|
|
11
|
+
reasoning: {
|
|
12
|
+
effort: options.reasoningEffort ?? INTELLIGENCE_REASONING_EFFORT,
|
|
13
|
+
},
|
|
14
|
+
store: false,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function normalizeRequest(request, options) {
|
|
18
|
+
const normalized = intelligenceRequest(request, {
|
|
19
|
+
model: options.model,
|
|
20
|
+
reasoningEffort: request.reasoning?.effort
|
|
21
|
+
?? options.reasoningEffort
|
|
22
|
+
?? INTELLIGENCE_REASONING_EFFORT,
|
|
23
|
+
});
|
|
24
|
+
delete normalized.temperature;
|
|
25
|
+
delete normalized.top_p;
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
export function buildIntelligenceResponses(apiKey, options = {}) {
|
|
29
|
+
if (!apiKey) {
|
|
30
|
+
throw new Error("OPENAI_API_KEY is not set");
|
|
31
|
+
}
|
|
32
|
+
const timeout = options.timeout ?? 180_000;
|
|
33
|
+
const provider = createOpenAi({
|
|
34
|
+
apiKey,
|
|
35
|
+
timeout,
|
|
36
|
+
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
37
|
+
...(options.baseURL ? { baseURL: options.baseURL } : {}),
|
|
38
|
+
});
|
|
39
|
+
const call = withRetry(provider.post.v1.responses, { retries: options.retries ?? 2, baseMs: 500 });
|
|
40
|
+
return async (request, signal) => {
|
|
41
|
+
const deadline = new AbortController();
|
|
42
|
+
const timer = setTimeout(() => deadline.abort(new Error(`OpenAI Responses request exceeded ${timeout}ms`)), timeout);
|
|
43
|
+
const combinedSignal = signal
|
|
44
|
+
? AbortSignal.any([signal, deadline.signal])
|
|
45
|
+
: deadline.signal;
|
|
46
|
+
try {
|
|
47
|
+
return await call(normalizeRequest(request, {
|
|
48
|
+
model: options.model,
|
|
49
|
+
reasoningEffort: options.reasoningEffort,
|
|
50
|
+
}), combinedSignal);
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (signal?.aborted) {
|
|
54
|
+
throw signal.reason instanceof Error
|
|
55
|
+
? signal.reason
|
|
56
|
+
: new Error("OpenAI Responses request was cancelled");
|
|
57
|
+
}
|
|
58
|
+
if (deadline.signal.aborted) {
|
|
59
|
+
throw deadline.signal.reason instanceof Error
|
|
60
|
+
? deadline.signal.reason
|
|
61
|
+
: new Error(`OpenAI Responses request exceeded ${timeout}ms`);
|
|
62
|
+
}
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
export function firstResponseText(response) {
|
|
71
|
+
if (response.error) {
|
|
72
|
+
throw new Error(`OpenAI Responses error ${response.error.code}: ${response.error.message}`);
|
|
73
|
+
}
|
|
74
|
+
if (response.status === "incomplete") {
|
|
75
|
+
const reason = response.incomplete_details?.reason ?? "unknown reason";
|
|
76
|
+
throw new Error(`OpenAI Responses output was incomplete: ${reason}`);
|
|
77
|
+
}
|
|
78
|
+
if (response.status !== "completed") {
|
|
79
|
+
throw new Error(`OpenAI Responses request did not complete (status=${response.status})`);
|
|
80
|
+
}
|
|
81
|
+
const texts = [];
|
|
82
|
+
for (const item of response.output) {
|
|
83
|
+
if (item.type !== "message")
|
|
84
|
+
continue;
|
|
85
|
+
for (const content of item.content) {
|
|
86
|
+
if (content.type === "refusal") {
|
|
87
|
+
throw new Error(`OpenAI Responses refused the request: ${content.refusal}`);
|
|
88
|
+
}
|
|
89
|
+
if (content.type === "output_text") {
|
|
90
|
+
texts.push(content.text);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const text = texts.join("").trim();
|
|
95
|
+
if (!text) {
|
|
96
|
+
throw new Error("OpenAI Responses returned no text content");
|
|
97
|
+
}
|
|
98
|
+
return text;
|
|
99
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { findAudioFile, findPrimaryMediaFile, findVideoFile } from "videobook-engine";
|
|
2
|
+
export function findOriginalMediaFile(files, kind) {
|
|
3
|
+
const originals = files.filter((file) => /^original\./i.test(file.name));
|
|
4
|
+
if (kind === "video")
|
|
5
|
+
return findVideoFile(originals);
|
|
6
|
+
if (kind === "audio") {
|
|
7
|
+
return findAudioFile(originals, kind)
|
|
8
|
+
?? originals.find((file) => /^original\.webm$/i.test(file.name))
|
|
9
|
+
?? null;
|
|
10
|
+
}
|
|
11
|
+
return findPrimaryMediaFile(files, kind);
|
|
12
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { join, extname } from "node:path";
|
|
2
|
+
const OSG_POSITIONS = [
|
|
3
|
+
"top-left", "top-center", "top-right",
|
|
4
|
+
"middle-left", "middle-center", "middle-right",
|
|
5
|
+
"bottom-left", "bottom-center", "bottom-right",
|
|
6
|
+
];
|
|
7
|
+
export function isOsgPosition(value) {
|
|
8
|
+
return OSG_POSITIONS.includes(value);
|
|
9
|
+
}
|
|
10
|
+
export function isOsgSize(value) {
|
|
11
|
+
return value === "small" || value === "medium" || value === "large";
|
|
12
|
+
}
|
|
13
|
+
// Wider than _image-inputs' set: OSG accepts svg since Remotion renders it in
|
|
14
|
+
// a browser, while provider uploads (the _image-inputs consumers) cannot.
|
|
15
|
+
const OSG_IMAGE_EXTENSIONS = new Set(["png", "jpg", "jpeg", "webp", "gif", "svg"]);
|
|
16
|
+
function findOsgImageFile(files) {
|
|
17
|
+
const images = files.filter((file) => {
|
|
18
|
+
const ext = (file.extension ?? extname(file.name)).replace(/^\./, "").toLowerCase();
|
|
19
|
+
return OSG_IMAGE_EXTENSIONS.has(ext);
|
|
20
|
+
});
|
|
21
|
+
// Manifests are alphabetically sorted, so a bare images[0] would pick a
|
|
22
|
+
// cover-crop preview variant (landscape.jpg) over the transparent source.
|
|
23
|
+
return images.find((file) => file.name.startsWith("original")) ?? images[0] ?? null;
|
|
24
|
+
}
|
|
25
|
+
/** Resolve an OSG image asset id to the on-disk path of its source image. */
|
|
26
|
+
export async function resolveOsgImagePath(engine, osgAssetId) {
|
|
27
|
+
const manifest = await engine.files.manifest(osgAssetId);
|
|
28
|
+
if (!manifest.ok) {
|
|
29
|
+
return { ok: false, error: `OSG asset error: ${manifest.error.message}` };
|
|
30
|
+
}
|
|
31
|
+
const imgFile = findOsgImageFile(manifest.value.files);
|
|
32
|
+
if (!imgFile) {
|
|
33
|
+
return { ok: false, error: `No image file found in OSG asset ${osgAssetId}` };
|
|
34
|
+
}
|
|
35
|
+
return { ok: true, path: join(manifest.value.path, imgFile.name) };
|
|
36
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
export const OSG_PLACEMENT_VERSION = 1;
|
|
4
|
+
const OSG_PLACEMENT_METADATA_KEY = "osg.placement";
|
|
5
|
+
const regionSchema = z.object({
|
|
6
|
+
kind: z.enum(["face", "text", "graphic"]),
|
|
7
|
+
confidence: z.number().min(0).max(1),
|
|
8
|
+
x: z.number().min(0).max(1),
|
|
9
|
+
y: z.number().min(0).max(1),
|
|
10
|
+
width: z.number().positive().max(1),
|
|
11
|
+
height: z.number().positive().max(1),
|
|
12
|
+
}).strict().refine((region) => region.x + region.width <= 1.001 && region.y + region.height <= 1.001, { message: "occupied region extends beyond the source frame" });
|
|
13
|
+
const positionSchema = z.enum([
|
|
14
|
+
"top-left", "top-center", "top-right",
|
|
15
|
+
"middle-left", "middle-center", "middle-right",
|
|
16
|
+
"bottom-left", "bottom-center", "bottom-right",
|
|
17
|
+
]);
|
|
18
|
+
const analysisSchema = z.object({
|
|
19
|
+
version: z.literal(OSG_PLACEMENT_VERSION),
|
|
20
|
+
fingerprint: z.string().min(1),
|
|
21
|
+
sourceWidth: z.number().positive(),
|
|
22
|
+
sourceHeight: z.number().positive(),
|
|
23
|
+
frameTimes: z.array(z.number().nonnegative()).min(1),
|
|
24
|
+
regions: z.array(regionSchema),
|
|
25
|
+
positions: z.object({
|
|
26
|
+
landscape: positionSchema,
|
|
27
|
+
portrait: positionSchema,
|
|
28
|
+
square: positionSchema,
|
|
29
|
+
}).strict(),
|
|
30
|
+
analyzedAt: z.string().min(1),
|
|
31
|
+
}).strict();
|
|
32
|
+
export function osgPlacementFingerprint(videoFile, frameNames, box) {
|
|
33
|
+
const payload = {
|
|
34
|
+
version: OSG_PLACEMENT_VERSION,
|
|
35
|
+
video: {
|
|
36
|
+
name: videoFile.name,
|
|
37
|
+
size: videoFile.sizeBytes,
|
|
38
|
+
objectHash: videoFile.objectHash,
|
|
39
|
+
},
|
|
40
|
+
frames: frameNames,
|
|
41
|
+
box: {
|
|
42
|
+
aspect: Number(box.aspect.toFixed(4)),
|
|
43
|
+
minDimFraction: box.minDimFraction,
|
|
44
|
+
userScale: Number(box.userScale.toFixed(2)),
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
48
|
+
}
|
|
49
|
+
export function parseOsgPlacementAnalysis(raw) {
|
|
50
|
+
return analysisSchema.parse(raw);
|
|
51
|
+
}
|
|
52
|
+
export async function readOsgPlacementAnalysis(engine, assetId) {
|
|
53
|
+
const result = await engine.metadata.artifacts.read(assetId, OSG_PLACEMENT_METADATA_KEY);
|
|
54
|
+
if (!result.ok)
|
|
55
|
+
return null;
|
|
56
|
+
const parsed = analysisSchema.safeParse(result.value);
|
|
57
|
+
return parsed.success ? parsed.data : null;
|
|
58
|
+
}
|
|
59
|
+
export async function writeOsgPlacementAnalysis(engine, assetId, analysis) {
|
|
60
|
+
const parsed = parseOsgPlacementAnalysis(analysis);
|
|
61
|
+
const result = await engine.metadata.artifacts.write(assetId, OSG_PLACEMENT_METADATA_KEY, parsed);
|
|
62
|
+
if (!result.ok)
|
|
63
|
+
throw new Error(`Failed to persist OSG placement analysis: ${result.error.message}`);
|
|
64
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
const ORIENTATIONS = ["landscape", "portrait", "square"];
|
|
2
|
+
/**
|
|
3
|
+
* Merge OSG fields into every orientation's `.render` metadata without
|
|
4
|
+
* triggering a render. `patch` receives the orientation and its existing
|
|
5
|
+
* metadata so callers can vary fields per orientation (the frame-aware
|
|
6
|
+
* watermark stores a different corner for each) or derive defaults from the
|
|
7
|
+
* stored values.
|
|
8
|
+
*/
|
|
9
|
+
export async function writeOsgSettings(engine, assetId, patch) {
|
|
10
|
+
const settings = {};
|
|
11
|
+
try {
|
|
12
|
+
for (const orientation of ORIENTATIONS) {
|
|
13
|
+
const existing = await engine.metadata.artifacts.read(assetId, `${orientation}.render`);
|
|
14
|
+
const base = existing.ok ? existing.value : {};
|
|
15
|
+
settings[orientation] = { ...base, ...patch(orientation, base) };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
return { ok: false, error: error instanceof Error ? error.message : String(error) };
|
|
20
|
+
}
|
|
21
|
+
for (const orientation of ORIENTATIONS) {
|
|
22
|
+
const written = await engine.metadata.artifacts.write(assetId, `${orientation}.render`, settings[orientation]);
|
|
23
|
+
if (!written.ok)
|
|
24
|
+
return { ok: false, error: written.error.message };
|
|
25
|
+
}
|
|
26
|
+
return { ok: true, settings };
|
|
27
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tools that spend provider money: generation, analysis, transcription,
|
|
3
|
+
* tagging, and the notebook run entry points that dispatch paid cells.
|
|
4
|
+
*/
|
|
5
|
+
export const BILLABLE_TOOL_NAMES = new Set([
|
|
6
|
+
"prepare_cold_open", "analyze_video", "analyze_jokes", "suggest_cuts", "suggest_sfx", "apply_sfx",
|
|
7
|
+
"transcribe", "translate_subtitles", "isolate_vocals", "clean_video_audio",
|
|
8
|
+
"make_cover_song", "extend_audio_asset", "rewrite_prompt", "rewrite_script",
|
|
9
|
+
"describe_image", "analyze_image_subjects", "edit_image_subjects", "rename_all",
|
|
10
|
+
"retag_asset", "verify_x_mentions",
|
|
11
|
+
"backfill_asset_tags", "retry_generation", "retry_job",
|
|
12
|
+
"run_notebook", "run_notebook_cell", "enqueue_notebook_run", "continue_notebook_generation",
|
|
13
|
+
"generate_image", "generate_video", "generate_audio",
|
|
14
|
+
]);
|
|
15
|
+
/**
|
|
16
|
+
* One answer to "does running this cost the user money", shared by the workspace
|
|
17
|
+
* action catalog's confirmation policy and the chain's per-step approval gate so
|
|
18
|
+
* the two can never drift apart.
|
|
19
|
+
*/
|
|
20
|
+
export function toolSpendsMoney(name) {
|
|
21
|
+
return BILLABLE_TOOL_NAMES.has(name);
|
|
22
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export function platformGuidance(p) {
|
|
2
|
+
switch (p) {
|
|
3
|
+
case "ig":
|
|
4
|
+
return ("PLATFORM: Instagram. Target 4:5 portrait or 1:1 square framing. " +
|
|
5
|
+
"Aesthetic-candid register — curated but unstaged: natural daylight or soft indoor light, " +
|
|
6
|
+
"warm/golden cast acceptable, light film grain tolerated. Mild VSCO-style lift in shadows; " +
|
|
7
|
+
"skin kept natural with no heavy beautify. Avoid HDR crunch, over-saturation, and harsh " +
|
|
8
|
+
"on-camera flash unless the image is clearly a flash-aesthetic shot. Default device read: " +
|
|
9
|
+
"iPhone main camera. Never use editorial/film-stock vocabulary unless the image is clearly " +
|
|
10
|
+
"a professional editorial photo.");
|
|
11
|
+
case "tiktok":
|
|
12
|
+
return ("PLATFORM: TikTok. Target 9:16 vertical framing. Raw phone snapshot energy — candid framing, " +
|
|
13
|
+
"slight motion softness, mild compression artifacts, auto-HDR lifted blacks. Indoor scenes: " +
|
|
14
|
+
"ring-light rim or warm tungsten acceptable; at night, harsh on-camera flash is on-brand. " +
|
|
15
|
+
"Front-camera selfies (with visible mirror/arm) are native to the platform. Skin unretouched, " +
|
|
16
|
+
"tiny chromatic aberration on high-contrast edges. Never apply editorial polish, cinematic " +
|
|
17
|
+
"color grading, or named film-stock vocabulary (Portra, Cinestill, etc.). No 'cinematic', " +
|
|
18
|
+
"'masterpiece', 'editorial' — this is a phone snapshot.");
|
|
19
|
+
case "youtube":
|
|
20
|
+
return ("PLATFORM: YouTube. Default to 16:9 landscape unless the image is clearly a Short (then 9:16). " +
|
|
21
|
+
"Slightly elevated production versus TikTok: punchier contrast, cleaner key light, faces " +
|
|
22
|
+
"expressive and readable at small thumbnail sizes. Colour can sit slightly saturated for " +
|
|
23
|
+
"thumbnail legibility but avoid over-stylised filters. Keep the result photographic rather " +
|
|
24
|
+
"than graphic-design; no logo overlays, no title text baked in.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
import { createDropbox } from "@apicity/dropbox";
|
|
4
|
+
import { appState } from "../app-state.js";
|
|
5
|
+
const KEY = "dropbox.oauth.tokens";
|
|
6
|
+
const REDIRECT = "http://127.0.0.1:8766/callback";
|
|
7
|
+
let pending = false;
|
|
8
|
+
let lastError = null;
|
|
9
|
+
let closeFlow = null;
|
|
10
|
+
let refreshing = null;
|
|
11
|
+
function oauthClient() {
|
|
12
|
+
const clientId = process.env.DROPBOX_CLIENT_ID;
|
|
13
|
+
if (!clientId)
|
|
14
|
+
throw new Error("Set DROPBOX_CLIENT_ID to your Dropbox app key before connecting");
|
|
15
|
+
return createDropbox({ clientId, clientSecret: process.env.DROPBOX_CLIENT_SECRET });
|
|
16
|
+
}
|
|
17
|
+
export async function dropboxClient() {
|
|
18
|
+
if (refreshing)
|
|
19
|
+
return refreshing;
|
|
20
|
+
refreshing = (async () => {
|
|
21
|
+
const tokens = await appState.get(KEY);
|
|
22
|
+
if (!tokens)
|
|
23
|
+
throw new Error("Connect Dropbox in Settings before uploading");
|
|
24
|
+
if (new Date(tokens.expiresAt).getTime() > Date.now() + 60_000)
|
|
25
|
+
return createDropbox({ oauthToken: tokens.accessToken });
|
|
26
|
+
const refreshed = await oauthClient().oauth2.token({ grant_type: "refresh_token", refresh_token: tokens.refreshToken });
|
|
27
|
+
await appState.set(KEY, { accessToken: refreshed.access_token, refreshToken: refreshed.refresh_token ?? tokens.refreshToken, expiresAt: new Date(Date.now() + refreshed.expires_in * 1000).toISOString() });
|
|
28
|
+
return createDropbox({ oauthToken: refreshed.access_token });
|
|
29
|
+
})();
|
|
30
|
+
try {
|
|
31
|
+
return await refreshing;
|
|
32
|
+
}
|
|
33
|
+
finally {
|
|
34
|
+
refreshing = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export async function dropboxStatus() {
|
|
38
|
+
const tokens = await appState.get(KEY);
|
|
39
|
+
return { connected: Boolean(tokens), pending, has_credentials: Boolean(process.env.DROPBOX_CLIENT_ID), error: lastError, redirect_uri: REDIRECT };
|
|
40
|
+
}
|
|
41
|
+
export async function startDropboxConnect() {
|
|
42
|
+
const client = oauthClient();
|
|
43
|
+
if (closeFlow)
|
|
44
|
+
throw new Error("Dropbox connection is already in progress. Complete it in the browser first.");
|
|
45
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
46
|
+
const state = randomBytes(24).toString("base64url");
|
|
47
|
+
const url = new URL("https://www.dropbox.com/oauth2/authorize");
|
|
48
|
+
url.search = new URLSearchParams({ client_id: process.env.DROPBOX_CLIENT_ID, response_type: "code", redirect_uri: REDIRECT, token_access_type: "offline", code_challenge: createHash("sha256").update(verifier).digest("base64url"), code_challenge_method: "S256", state }).toString();
|
|
49
|
+
const server = createServer((req, res) => {
|
|
50
|
+
const callback = new URL(req.url ?? "/", REDIRECT);
|
|
51
|
+
if (callback.pathname !== "/callback") {
|
|
52
|
+
res.writeHead(404).end();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (callback.searchParams.get("state") !== state || !callback.searchParams.get("code")) {
|
|
56
|
+
res.writeHead(400).end("Invalid authorization callback");
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
void (async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await client.oauth2.token({ grant_type: "authorization_code", code: callback.searchParams.get("code"), redirect_uri: REDIRECT, code_verifier: verifier });
|
|
62
|
+
if (closeFlow !== closeCurrent) {
|
|
63
|
+
res.writeHead(400).end("Authorization expired. Connect again from Videobook.");
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
if (!result.refresh_token)
|
|
67
|
+
throw new Error("Dropbox did not grant offline access");
|
|
68
|
+
await appState.set(KEY, { accessToken: result.access_token, refreshToken: result.refresh_token, expiresAt: new Date(Date.now() + result.expires_in * 1000).toISOString() });
|
|
69
|
+
res.writeHead(200, { "Content-Type": "text/plain; charset=utf-8" }).end("Dropbox connected. Return to Videobook.");
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
lastError = "Dropbox authorization failed. Try connecting again.";
|
|
73
|
+
res.writeHead(400).end(lastError);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
closeCurrent();
|
|
77
|
+
}
|
|
78
|
+
})();
|
|
79
|
+
});
|
|
80
|
+
const timeout = setTimeout(() => { lastError = "Dropbox connection timed out"; closeCurrent(); }, 5 * 60_000);
|
|
81
|
+
timeout.unref();
|
|
82
|
+
const closeCurrent = () => { clearTimeout(timeout); server.close(); if (closeFlow === closeCurrent) {
|
|
83
|
+
pending = false;
|
|
84
|
+
closeFlow = null;
|
|
85
|
+
} };
|
|
86
|
+
closeFlow = closeCurrent;
|
|
87
|
+
await new Promise((resolve, reject) => { server.once("error", reject); server.listen(8766, "127.0.0.1", resolve); }).catch((error) => { closeCurrent(); throw error; });
|
|
88
|
+
pending = true;
|
|
89
|
+
lastError = null;
|
|
90
|
+
return { authorize_url: url.toString() };
|
|
91
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { extname } from "node:path";
|
|
3
|
+
import { DropboxError } from "@apicity/dropbox";
|
|
4
|
+
import { dropboxClient } from "./dropbox-auth.js";
|
|
5
|
+
import { freezePostOutbound, getPost, patchPostRuntime, readPostRuntime, updatePost } from "./post-store.js";
|
|
6
|
+
const CHUNK = 8 * 1024 * 1024;
|
|
7
|
+
export function dropboxContentHash(bytes) {
|
|
8
|
+
const hashes = [];
|
|
9
|
+
for (let offset = 0; offset < bytes.length; offset += 4 * 1024 * 1024)
|
|
10
|
+
hashes.push(createHash("sha256").update(bytes.subarray(offset, offset + 4 * 1024 * 1024)).digest());
|
|
11
|
+
return createHash("sha256").update(Buffer.concat(hashes)).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function correctOffset(value) {
|
|
14
|
+
if (!value || typeof value !== "object")
|
|
15
|
+
return null;
|
|
16
|
+
const object = value;
|
|
17
|
+
if (typeof object.correct_offset === "number")
|
|
18
|
+
return object.correct_offset;
|
|
19
|
+
for (const child of Object.values(object)) {
|
|
20
|
+
const result = correctOffset(child);
|
|
21
|
+
if (result !== null)
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
async function uploadFile(client, bytes, initial, persist, signal) {
|
|
27
|
+
let state = { ...initial };
|
|
28
|
+
const save = (patch) => { state = { ...state, ...patch }; persist(state); };
|
|
29
|
+
try {
|
|
30
|
+
const metadata = await client.files.getMetadata({ path: state.path }, signal);
|
|
31
|
+
if (metadata[".tag"] === "file" && metadata.content_hash === state.hash) {
|
|
32
|
+
save({ id: String(metadata.id), rev: String(metadata.rev), offset: bytes.length });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
throw new Error("The Dropbox destination already contains different content. Choose a new destination folder.");
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (!(error instanceof DropboxError) || !error.error_summary?.startsWith("path/not_found"))
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
if (state.rev)
|
|
42
|
+
throw new Error("A previously uploaded Dropbox file was removed. Choose a new destination folder.");
|
|
43
|
+
for (let recovery = 0; recovery < 4; recovery += 1) {
|
|
44
|
+
try {
|
|
45
|
+
if (!state.sessionId) {
|
|
46
|
+
const session = await client.files.uploadSession.start({ contents: new Uint8Array() }, signal);
|
|
47
|
+
save({ sessionId: session.session_id, offset: 0 });
|
|
48
|
+
}
|
|
49
|
+
while (state.offset < bytes.length) {
|
|
50
|
+
const end = Math.min(state.offset + CHUNK, bytes.length);
|
|
51
|
+
await client.files.uploadSession.appendV2({ contents: new Uint8Array(bytes.subarray(state.offset, end)), cursor: { session_id: state.sessionId, offset: state.offset } }, signal);
|
|
52
|
+
save({ offset: end });
|
|
53
|
+
}
|
|
54
|
+
const finished = await client.files.uploadSession.finish({ contents: new Uint8Array(), cursor: { session_id: state.sessionId, offset: state.offset }, commit: { path: state.path, mode: "add", autorename: false, mute: true, strict_conflict: true } }, signal);
|
|
55
|
+
if (finished.content_hash && finished.content_hash !== state.hash)
|
|
56
|
+
throw new Error("Dropbox returned different file content. Choose a new destination folder.");
|
|
57
|
+
save({ id: finished.id, rev: finished.rev, offset: bytes.length });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
if (!(error instanceof DropboxError))
|
|
62
|
+
throw error;
|
|
63
|
+
const offset = correctOffset(error.error);
|
|
64
|
+
if (offset !== null && offset >= 0 && offset <= bytes.length) {
|
|
65
|
+
save({ offset });
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (error.error_summary?.includes("not_found") || error.error_summary?.includes("closed") || error.error_summary?.includes("conflict")) {
|
|
69
|
+
try {
|
|
70
|
+
const metadata = await client.files.getMetadata({ path: state.path }, signal);
|
|
71
|
+
if (metadata[".tag"] === "file" && metadata.content_hash === state.hash) {
|
|
72
|
+
save({ id: String(metadata.id), rev: String(metadata.rev), offset: bytes.length });
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
throw new Error("Dropbox destination content changed");
|
|
76
|
+
}
|
|
77
|
+
catch (lookup) {
|
|
78
|
+
if (!(lookup instanceof DropboxError) || !lookup.error_summary?.startsWith("path/not_found"))
|
|
79
|
+
throw lookup;
|
|
80
|
+
}
|
|
81
|
+
save({ sessionId: undefined, offset: 0 });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
throw new Error("Dropbox upload could not reconcile its saved position. Retry the upload.");
|
|
88
|
+
}
|
|
89
|
+
async function applyCaption(engine, id, caption) {
|
|
90
|
+
const post = getPost(engine, id);
|
|
91
|
+
if (caption === undefined || caption === post.definition.caption)
|
|
92
|
+
return post;
|
|
93
|
+
if (post.runtime.outbound)
|
|
94
|
+
throw new Error("This Post's caption was frozen at its first delivery. Create a new Post to deliver a different caption.");
|
|
95
|
+
return updatePost(engine, id, { caption });
|
|
96
|
+
}
|
|
97
|
+
export async function uploadPostToDropbox(engine, id, options = {}) {
|
|
98
|
+
if (["posting", "needs_reconciliation", "scheduled"].includes(getPost(engine, id).runtime.status))
|
|
99
|
+
throw new Error("Resolve or cancel the current publication before uploading");
|
|
100
|
+
const post = await applyCaption(engine, id, options.caption);
|
|
101
|
+
const outbound = await freezePostOutbound(engine, id);
|
|
102
|
+
const folder = options.folder?.replace(/\/+$/, "") ?? post.runtime.dropbox?.folder ?? `/Videobook/${engine.book.get().name}/${id}`;
|
|
103
|
+
if (!folder.startsWith("/") || folder.includes("..") || [...folder].some(character => character.charCodeAt(0) < 32))
|
|
104
|
+
throw new Error("Use an absolute Dropbox folder without .. segments");
|
|
105
|
+
const media = await engine.files.readAtRevision(outbound.artifactId, outbound.filename, outbound.revision);
|
|
106
|
+
if (!media.ok)
|
|
107
|
+
throw new Error(media.error.message);
|
|
108
|
+
const caption = Buffer.from(outbound.caption);
|
|
109
|
+
const existing = post.runtime.dropbox?.folder === folder ? post.runtime.dropbox : undefined;
|
|
110
|
+
patchPostRuntime(engine, id, { status: "uploading", error: null, dropbox: existing ?? {
|
|
111
|
+
folder,
|
|
112
|
+
video: { path: `${folder}/clip${extname(outbound.filename)}`, hash: dropboxContentHash(media.value), offset: 0 },
|
|
113
|
+
caption: { path: `${folder}/caption.txt`, hash: dropboxContentHash(caption), offset: 0 },
|
|
114
|
+
} });
|
|
115
|
+
try {
|
|
116
|
+
const client = options.client ?? await dropboxClient();
|
|
117
|
+
for (const [name, bytes] of [["video", media.value], ["caption", caption]]) {
|
|
118
|
+
const state = readPostRuntime(engine, id).dropbox;
|
|
119
|
+
await uploadFile(client, bytes, state[name], (file) => {
|
|
120
|
+
const current = readPostRuntime(engine, id).dropbox;
|
|
121
|
+
patchPostRuntime(engine, id, { dropbox: { ...current, [name]: file } });
|
|
122
|
+
}, options.signal);
|
|
123
|
+
}
|
|
124
|
+
patchPostRuntime(engine, id, { status: post.runtime.url ? post.runtime.status : "ready", error: null, dropbox: { ...readPostRuntime(engine, id).dropbox, completedAt: new Date().toISOString() } });
|
|
125
|
+
return getPost(engine, id);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
const message = error instanceof DropboxError ? `Dropbox upload failed (${error.status}). ${error.status === 401 ? "Reconnect Dropbox and retry." : "Retry to resume the saved upload."}` : error instanceof Error ? error.message : "Dropbox upload failed";
|
|
129
|
+
patchPostRuntime(engine, id, { status: post.runtime.url ? post.runtime.status : "failed", error: message });
|
|
130
|
+
throw new Error(message);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { copyFile, mkdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
5
|
+
import { freezePostOutbound, getPost, patchPostRuntime, readPostRuntime, updatePost } from "./post-store.js";
|
|
6
|
+
import { defaultPostFolderName, icloudFolderError } from "./post-folder.js";
|
|
7
|
+
import { readUbiquitousStates, summarizeUbiquitousSync } from "./icloud-status.js";
|
|
8
|
+
function icloudRoot() {
|
|
9
|
+
return process.env.VIDEOBOOK_ICLOUD_DIR || join(homedir(), "Library/Mobile Documents/com~apple~CloudDocs/Videobook");
|
|
10
|
+
}
|
|
11
|
+
async function isDirectory(path) {
|
|
12
|
+
try {
|
|
13
|
+
return (await stat(path)).isDirectory();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function fileSize(path) {
|
|
20
|
+
try {
|
|
21
|
+
const info = await stat(path);
|
|
22
|
+
return info.isFile() ? info.size : -1;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
|
|
26
|
+
return null;
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
async function icloudUnavailableReason(root = icloudRoot()) {
|
|
31
|
+
if (process.platform !== "darwin" && !process.env.VIDEOBOOK_ICLOUD_DIR)
|
|
32
|
+
return "Saving to iCloud needs the Videobook server running on a Mac.";
|
|
33
|
+
if (!await isDirectory(dirname(root)))
|
|
34
|
+
return "iCloud Drive is not set up on this Mac. Turn on iCloud Drive in System Settings.";
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
function insideIcloudRoot(root, relativePath) {
|
|
38
|
+
const rootResolved = resolve(root);
|
|
39
|
+
const target = resolve(rootResolved, relativePath);
|
|
40
|
+
if (target !== rootResolved && !target.startsWith(`${rootResolved}${sep}`))
|
|
41
|
+
throw new Error("iCloud path is outside the Videobook folder.");
|
|
42
|
+
return target;
|
|
43
|
+
}
|
|
44
|
+
export async function postIcloudSyncStatus(engine, id) {
|
|
45
|
+
const recorded = getPost(engine, id).runtime.icloud;
|
|
46
|
+
const relative = [recorded?.video?.path, recorded?.caption?.path].filter((path) => Boolean(path));
|
|
47
|
+
if (relative.length === 0)
|
|
48
|
+
return { phase: "not-icloud", error: null };
|
|
49
|
+
const root = icloudRoot();
|
|
50
|
+
return summarizeUbiquitousSync(await readUbiquitousStates(relative.map((path) => insideIcloudRoot(root, path))));
|
|
51
|
+
}
|
|
52
|
+
export async function icloudStatus() {
|
|
53
|
+
const root = icloudRoot();
|
|
54
|
+
const reason = await icloudUnavailableReason(root);
|
|
55
|
+
const state = reason ? null : (await readUbiquitousStates([root]))?.[0] ?? null;
|
|
56
|
+
return { available: !reason, root, shared: state?.ubiquitous ? state.shared : null, role: state?.ubiquitous ? state.role : null, reason };
|
|
57
|
+
}
|
|
58
|
+
async function applyCaption(engine, id, caption) {
|
|
59
|
+
const post = getPost(engine, id);
|
|
60
|
+
if (caption === undefined || caption === post.definition.caption)
|
|
61
|
+
return post;
|
|
62
|
+
if (post.runtime.outbound)
|
|
63
|
+
throw new Error("This Post's caption was frozen at its first delivery. Create a new Post to deliver a different caption.");
|
|
64
|
+
return updatePost(engine, id, { caption });
|
|
65
|
+
}
|
|
66
|
+
async function placeFile(target, bytes) {
|
|
67
|
+
const stagingDir = join(homedir(), "Library/Caches/videobook/icloud-staging");
|
|
68
|
+
await mkdir(stagingDir, { recursive: true });
|
|
69
|
+
const staging = join(stagingDir, randomUUID());
|
|
70
|
+
await writeFile(staging, bytes);
|
|
71
|
+
try {
|
|
72
|
+
await rename(staging, target);
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "EXDEV"))
|
|
76
|
+
throw error;
|
|
77
|
+
const partial = join(dirname(target), `.${target.split("/").at(-1)}.videobook-partial`);
|
|
78
|
+
await copyFile(staging, partial);
|
|
79
|
+
await rename(partial, target);
|
|
80
|
+
}
|
|
81
|
+
finally {
|
|
82
|
+
await rm(staging, { force: true });
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
async function deliverFile(root, path, bytes, recorded) {
|
|
86
|
+
const file = { path, sha256: createHash("sha256").update(bytes).digest("hex"), size: bytes.length };
|
|
87
|
+
const target = join(root, path);
|
|
88
|
+
const existing = await fileSize(target);
|
|
89
|
+
if (existing !== null) {
|
|
90
|
+
if (recorded && recorded.path === file.path && recorded.sha256 === file.sha256 && recorded.size === file.size && existing === file.size)
|
|
91
|
+
return recorded;
|
|
92
|
+
throw new Error(`iCloud folder already has a different ${path.split("/").at(-1)}. Choose another folder.`);
|
|
93
|
+
}
|
|
94
|
+
await placeFile(target, bytes);
|
|
95
|
+
return file;
|
|
96
|
+
}
|
|
97
|
+
export async function savePostToICloud(engine, id, options = {}) {
|
|
98
|
+
if (["posting", "needs_reconciliation", "scheduled"].includes(getPost(engine, id).runtime.status))
|
|
99
|
+
throw new Error("Resolve or cancel the current publication before saving to iCloud");
|
|
100
|
+
const root = icloudRoot();
|
|
101
|
+
const unavailable = await icloudUnavailableReason(root);
|
|
102
|
+
if (unavailable)
|
|
103
|
+
throw new Error(unavailable);
|
|
104
|
+
const current = getPost(engine, id);
|
|
105
|
+
const folder = (options.folder ?? current.runtime.icloud?.folder ?? defaultPostFolderName(engine.book.get().name, current.address, id)).trim().replace(/\/+$/u, "");
|
|
106
|
+
const folderError = icloudFolderError(folder);
|
|
107
|
+
if (folderError)
|
|
108
|
+
throw new Error(folderError);
|
|
109
|
+
const post = await applyCaption(engine, id, options.caption);
|
|
110
|
+
const outbound = await freezePostOutbound(engine, id);
|
|
111
|
+
const media = await engine.files.readAtRevision(outbound.artifactId, outbound.filename, outbound.revision);
|
|
112
|
+
if (!media.ok)
|
|
113
|
+
throw new Error(media.error.message);
|
|
114
|
+
const existing = post.runtime.icloud?.folder === folder ? post.runtime.icloud : undefined;
|
|
115
|
+
const state = existing ? { ...existing } : { folder };
|
|
116
|
+
delete state.completedAt;
|
|
117
|
+
patchPostRuntime(engine, id, { status: post.runtime.url ? post.runtime.status : "uploading", error: null, icloud: state });
|
|
118
|
+
try {
|
|
119
|
+
await mkdir(join(root, folder), { recursive: true });
|
|
120
|
+
const files = [["video", `${folder}/clip${extname(outbound.filename)}`, media.value], ["caption", `${folder}/caption.txt`, Buffer.from(outbound.caption)]];
|
|
121
|
+
for (const [name, path, bytes] of files) {
|
|
122
|
+
const delivered = await deliverFile(root, path, bytes, state[name]);
|
|
123
|
+
state[name] = delivered;
|
|
124
|
+
patchPostRuntime(engine, id, { icloud: { ...readPostRuntime(engine, id).icloud, [name]: delivered } });
|
|
125
|
+
}
|
|
126
|
+
patchPostRuntime(engine, id, { status: post.runtime.url ? post.runtime.status : "ready", error: null, icloud: { ...readPostRuntime(engine, id).icloud, completedAt: new Date().toISOString() } });
|
|
127
|
+
return getPost(engine, id);
|
|
128
|
+
}
|
|
129
|
+
catch (error) {
|
|
130
|
+
const message = error instanceof Error ? error.message : "Saving to iCloud failed";
|
|
131
|
+
patchPostRuntime(engine, id, { status: post.runtime.url ? post.runtime.status : "failed", error: message });
|
|
132
|
+
throw new Error(message);
|
|
133
|
+
}
|
|
134
|
+
}
|