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,456 @@
|
|
|
1
|
+
import { findPrimaryMediaFile } from "videobook-engine";
|
|
2
|
+
import { createX, XError } from "@apicity/x";
|
|
3
|
+
import { extname, join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { readFile, unlink } from "node:fs/promises";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { getValidAccessToken } from "./x-tokens.js";
|
|
8
|
+
import { readXAccount, X_POST_MAX_CHARACTERS, xPostMaxCharacters, } from "./account.js";
|
|
9
|
+
import { ensureXPost } from "../posts/x-post-target.js";
|
|
10
|
+
import { freezePostOutbound, getPost, patchPostRuntime, readPostRuntime } from "../posts/post-store.js";
|
|
11
|
+
import { withNotebookMutation } from "../notebook-mutations.js";
|
|
12
|
+
import { appendXPostMetadata } from "./post-metadata.js";
|
|
13
|
+
import { AUDIO_OPTS, H264_MEDIUM, probeVideo, runFfmpeg } from "../ffmpeg.js";
|
|
14
|
+
import { logger } from "../logger.js";
|
|
15
|
+
const APPEND_CHUNK_BYTES = 4 * 1024 * 1024;
|
|
16
|
+
const STATUS_POLL_TIMEOUT_MS = 5 * 60_000;
|
|
17
|
+
export const STATUS_POLL_MIN_INTERVAL_MS = 2_000;
|
|
18
|
+
async function defaultStatusPollSleep(ms, signal) {
|
|
19
|
+
await new Promise((resolve, reject) => {
|
|
20
|
+
const t = setTimeout(resolve, ms);
|
|
21
|
+
if (!signal)
|
|
22
|
+
return;
|
|
23
|
+
const onAbort = () => {
|
|
24
|
+
clearTimeout(t);
|
|
25
|
+
reject(new Error("Aborted"));
|
|
26
|
+
};
|
|
27
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
let statusPollSleep = defaultStatusPollSleep;
|
|
31
|
+
export function configureXStatusPollForTests(options) {
|
|
32
|
+
statusPollSleep = options?.sleep ?? defaultStatusPollSleep;
|
|
33
|
+
}
|
|
34
|
+
function inferMediaType(filename) {
|
|
35
|
+
const ext = extname(filename).toLowerCase();
|
|
36
|
+
switch (ext) {
|
|
37
|
+
case ".mp4":
|
|
38
|
+
return { mediaType: "video/mp4", mediaCategory: "tweet_video" };
|
|
39
|
+
case ".mov":
|
|
40
|
+
return { mediaType: "video/quicktime", mediaCategory: "tweet_video" };
|
|
41
|
+
case ".jpg":
|
|
42
|
+
case ".jpeg":
|
|
43
|
+
return { mediaType: "image/jpeg", mediaCategory: "tweet_image" };
|
|
44
|
+
case ".png":
|
|
45
|
+
return { mediaType: "image/png", mediaCategory: "tweet_image" };
|
|
46
|
+
case ".webp":
|
|
47
|
+
return { mediaType: "image/webp", mediaCategory: "tweet_image" };
|
|
48
|
+
case ".gif":
|
|
49
|
+
return { mediaType: "image/gif", mediaCategory: "tweet_gif" };
|
|
50
|
+
default:
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function maybeReorientVideo(inputPath, orientation) {
|
|
55
|
+
if (!orientation)
|
|
56
|
+
return null;
|
|
57
|
+
const probe = await probeVideo(inputPath);
|
|
58
|
+
if (!probe.width || !probe.height)
|
|
59
|
+
return null;
|
|
60
|
+
const sourceMatches = orientation === "portrait"
|
|
61
|
+
? probe.height >= probe.width
|
|
62
|
+
: probe.width >= probe.height;
|
|
63
|
+
if (sourceMatches)
|
|
64
|
+
return null;
|
|
65
|
+
const targetW = orientation === "portrait" ? 1080 : 1920;
|
|
66
|
+
const targetH = orientation === "portrait" ? 1920 : 1080;
|
|
67
|
+
const filter = `scale=${targetW}:${targetH}:force_original_aspect_ratio=decrease,`
|
|
68
|
+
+ `pad=${targetW}:${targetH}:(ow-iw)/2:(oh-ih)/2:black,setsar=1`;
|
|
69
|
+
const tempPath = join(tmpdir(), `videocity-orient-${randomUUID()}.mp4`);
|
|
70
|
+
const args = [
|
|
71
|
+
"-y",
|
|
72
|
+
"-i",
|
|
73
|
+
inputPath,
|
|
74
|
+
"-vf",
|
|
75
|
+
filter,
|
|
76
|
+
...H264_MEDIUM,
|
|
77
|
+
"-pix_fmt",
|
|
78
|
+
"yuv420p",
|
|
79
|
+
"-movflags",
|
|
80
|
+
"+faststart",
|
|
81
|
+
];
|
|
82
|
+
if (probe.hasAudio)
|
|
83
|
+
args.push(...AUDIO_OPTS);
|
|
84
|
+
else
|
|
85
|
+
args.push("-an");
|
|
86
|
+
args.push(tempPath);
|
|
87
|
+
await runFfmpeg(args);
|
|
88
|
+
const buffer = await readFile(tempPath);
|
|
89
|
+
return { buffer, tempPath, mediaType: "video/mp4" };
|
|
90
|
+
}
|
|
91
|
+
function tweetUrl(id) {
|
|
92
|
+
return `https://x.com/i/web/status/${id}`;
|
|
93
|
+
}
|
|
94
|
+
async function prepareMedia(engine, assetId, orientation) {
|
|
95
|
+
const artifactResult = engine.artifacts.get(assetId);
|
|
96
|
+
if (!artifactResult.ok) {
|
|
97
|
+
throw new Error(`Asset ${assetId} not found: ${artifactResult.error.message}`);
|
|
98
|
+
}
|
|
99
|
+
const manifestResult = await engine.files.manifest(assetId);
|
|
100
|
+
if (!manifestResult.ok) {
|
|
101
|
+
throw new Error(`Asset ${assetId} not found: ${manifestResult.error.message}`);
|
|
102
|
+
}
|
|
103
|
+
const manifest = manifestResult.value;
|
|
104
|
+
const file = findPrimaryMediaFile(manifest.files, artifactResult.value.kind);
|
|
105
|
+
if (!file) {
|
|
106
|
+
throw new Error(`Asset ${assetId} has no primary media file to post`);
|
|
107
|
+
}
|
|
108
|
+
const info = inferMediaType(file.name);
|
|
109
|
+
if (!info) {
|
|
110
|
+
throw new Error(`Cannot post asset ${assetId}: unsupported media file ${file.name}`);
|
|
111
|
+
}
|
|
112
|
+
if (orientation && info.mediaCategory === "tweet_video") {
|
|
113
|
+
const transformed = await maybeReorientVideo(join(manifest.path, file.name), orientation);
|
|
114
|
+
if (transformed) {
|
|
115
|
+
return {
|
|
116
|
+
assetId,
|
|
117
|
+
buffer: transformed.buffer,
|
|
118
|
+
info: { mediaType: transformed.mediaType, mediaCategory: "tweet_video" },
|
|
119
|
+
tempPath: transformed.tempPath,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const readResult = await engine.files.read(assetId, file.name);
|
|
124
|
+
if (!readResult.ok) {
|
|
125
|
+
throw new Error(`Failed to read ${file.name}: ${readResult.error.message}`);
|
|
126
|
+
}
|
|
127
|
+
return { assetId, buffer: readResult.value, info, tempPath: null };
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* The post length the connected account allows. Text within the standard 280
|
|
131
|
+
* needs no lookup; anything longer is checked against the cached tier, since
|
|
132
|
+
* only X Premium accounts may post up to 25,000 characters.
|
|
133
|
+
*/
|
|
134
|
+
async function postCharacterLimit(text, followups, store) {
|
|
135
|
+
const longest = Math.max(text.length, ...followups.map((followup) => followup.text?.length ?? 0));
|
|
136
|
+
if (longest <= X_POST_MAX_CHARACTERS)
|
|
137
|
+
return X_POST_MAX_CHARACTERS;
|
|
138
|
+
const account = await readXAccount(store);
|
|
139
|
+
return xPostMaxCharacters(account?.subscription_type ?? null);
|
|
140
|
+
}
|
|
141
|
+
function characterLimitError(label, maxCharacters) {
|
|
142
|
+
const hint = maxCharacters === X_POST_MAX_CHARACTERS
|
|
143
|
+
? " (longer posts need an X Premium subscription)"
|
|
144
|
+
: "";
|
|
145
|
+
return new Error(`${label} exceeds ${maxCharacters} characters${hint}`);
|
|
146
|
+
}
|
|
147
|
+
async function prepareFollowups(engine, followups, maxCharacters) {
|
|
148
|
+
if (followups.length > 25) {
|
|
149
|
+
throw new Error("X threads support at most 25 follow-up posts here");
|
|
150
|
+
}
|
|
151
|
+
const prepared = [];
|
|
152
|
+
for (const [index, followup] of followups.entries()) {
|
|
153
|
+
const text = followup.text?.trim() ? followup.text : undefined;
|
|
154
|
+
const assetIds = followup.image_asset_ids ?? [];
|
|
155
|
+
if (!text && assetIds.length === 0) {
|
|
156
|
+
throw new Error(`Follow-up ${index + 1} needs text or an image`);
|
|
157
|
+
}
|
|
158
|
+
if ((followup.text?.length ?? 0) > maxCharacters) {
|
|
159
|
+
throw characterLimitError(`Follow-up ${index + 1}`, maxCharacters);
|
|
160
|
+
}
|
|
161
|
+
if (assetIds.length > 4) {
|
|
162
|
+
throw new Error(`Follow-up ${index + 1} has more than 4 images`);
|
|
163
|
+
}
|
|
164
|
+
const media = await Promise.all(assetIds.map(async (sourceAssetId) => await prepareMedia(engine, sourceAssetId)));
|
|
165
|
+
if (media.some((item) => item.info.mediaCategory === "tweet_video")) {
|
|
166
|
+
throw new Error(`Follow-up ${index + 1} attachments must be images`);
|
|
167
|
+
}
|
|
168
|
+
const gifs = media.filter((item) => item.info.mediaCategory === "tweet_gif");
|
|
169
|
+
if (gifs.length > 0 && media.length > 1) {
|
|
170
|
+
throw new Error(`Follow-up ${index + 1} can attach one GIF or up to four photos`);
|
|
171
|
+
}
|
|
172
|
+
prepared.push({ ...(text ? { text } : {}), media });
|
|
173
|
+
}
|
|
174
|
+
return prepared;
|
|
175
|
+
}
|
|
176
|
+
async function getXClient(_fs, injected) {
|
|
177
|
+
if (injected)
|
|
178
|
+
return injected;
|
|
179
|
+
const accessToken = await getValidAccessToken();
|
|
180
|
+
return createX({ accessToken });
|
|
181
|
+
}
|
|
182
|
+
async function uploadMedia(x, buffer, info, signal) {
|
|
183
|
+
const initRes = await x.post.v2.media.upload.initialize({
|
|
184
|
+
media_type: info.mediaType,
|
|
185
|
+
total_bytes: buffer.byteLength,
|
|
186
|
+
media_category: info.mediaCategory,
|
|
187
|
+
}, signal);
|
|
188
|
+
const mediaId = initRes.data.id;
|
|
189
|
+
const totalChunks = Math.max(1, Math.ceil(buffer.byteLength / APPEND_CHUNK_BYTES));
|
|
190
|
+
for (let i = 0; i < totalChunks; i++) {
|
|
191
|
+
const start = i * APPEND_CHUNK_BYTES;
|
|
192
|
+
const end = Math.min(buffer.byteLength, start + APPEND_CHUNK_BYTES);
|
|
193
|
+
const chunk = buffer.subarray(start, end);
|
|
194
|
+
const blob = new Blob([Uint8Array.from(chunk)], { type: info.mediaType });
|
|
195
|
+
await x.post.v2.media.upload.append(mediaId, { media: blob, segment_index: i }, signal);
|
|
196
|
+
}
|
|
197
|
+
const finalizeRes = await x.post.v2.media.upload.finalize(mediaId, signal);
|
|
198
|
+
await waitForProcessing(x, mediaId, finalizeRes.data.processing_info, signal);
|
|
199
|
+
return mediaId;
|
|
200
|
+
}
|
|
201
|
+
async function waitForProcessing(x, mediaId, initial, signal) {
|
|
202
|
+
if (!initial)
|
|
203
|
+
return;
|
|
204
|
+
if (initial.state === "succeeded")
|
|
205
|
+
return;
|
|
206
|
+
if (initial.state === "failed") {
|
|
207
|
+
throw new Error(`X media processing failed: ${initial.error?.message ?? "unknown"}`);
|
|
208
|
+
}
|
|
209
|
+
const deadline = Date.now() + STATUS_POLL_TIMEOUT_MS;
|
|
210
|
+
let next = initial;
|
|
211
|
+
while (next.state === "pending" || next.state === "in_progress") {
|
|
212
|
+
if (Date.now() > deadline) {
|
|
213
|
+
throw new Error(`X media processing timed out after ${STATUS_POLL_TIMEOUT_MS}ms`);
|
|
214
|
+
}
|
|
215
|
+
const waitSecs = Math.max(STATUS_POLL_MIN_INTERVAL_MS / 1000, next.check_after_secs ?? 2);
|
|
216
|
+
await statusPollSleep(waitSecs * 1000, signal);
|
|
217
|
+
const status = await x.get.v2.media.upload(mediaId, signal);
|
|
218
|
+
const info = status.data.processing_info;
|
|
219
|
+
if (!info)
|
|
220
|
+
return;
|
|
221
|
+
next = info;
|
|
222
|
+
}
|
|
223
|
+
if (next.state !== "succeeded") {
|
|
224
|
+
throw new Error(`X media processing failed: ${next.error?.message ?? next.state}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
export async function prepareXPublication(args) {
|
|
228
|
+
if (args.postCellId)
|
|
229
|
+
return withNotebookMutation(`post:${args.postCellId}`, () => prepareXPublicationMedia(args));
|
|
230
|
+
if (!args.publicationKey)
|
|
231
|
+
return prepareXPublicationMedia(args);
|
|
232
|
+
const key = `application.post-job.v1.${args.publicationKey}`;
|
|
233
|
+
return withNotebookMutation(`publication:${args.bookId}:${args.publicationKey}`, async () => {
|
|
234
|
+
const existing = args.engine.settings.get(key);
|
|
235
|
+
if (existing)
|
|
236
|
+
return withNotebookMutation(`post:${existing}`, () => prepareXPublicationMedia({ ...args, postCellId: existing }));
|
|
237
|
+
const post = await prepareXPublicationMedia(args);
|
|
238
|
+
const saved = args.engine.settings.set(key, post.id);
|
|
239
|
+
if (!saved.ok)
|
|
240
|
+
throw new Error(saved.error.message);
|
|
241
|
+
return post;
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
async function prepareXPublicationMedia(args) {
|
|
245
|
+
if (!args.postCellId && !args.text?.trim())
|
|
246
|
+
throw new Error("A post title is required");
|
|
247
|
+
if (args.text) {
|
|
248
|
+
const limit = await postCharacterLimit(args.text, args.followups ?? [], args.store);
|
|
249
|
+
if (args.text.length > limit)
|
|
250
|
+
throw characterLimitError("Post title", limit);
|
|
251
|
+
for (const [index, reply] of (args.followups ?? []).entries())
|
|
252
|
+
if ((reply.text?.length ?? 0) > limit)
|
|
253
|
+
throw characterLimitError(`Follow-up ${index + 1}`, limit);
|
|
254
|
+
}
|
|
255
|
+
const post = await ensureXPost(args.engine, args.assetId, args.text, args.postCellId);
|
|
256
|
+
if (post.runtime.xRequest) {
|
|
257
|
+
const normalize = (replies) => JSON.stringify(replies.map(reply => ({ text: reply.text?.trim() ? reply.text : undefined, image_asset_ids: reply.image_asset_ids ?? [] })));
|
|
258
|
+
if (args.orientation && args.orientation !== post.runtime.xRequest.orientation
|
|
259
|
+
|| args.followups && normalize(args.followups) !== normalize(post.runtime.xRequest.sourceFollowups ?? []))
|
|
260
|
+
throw new Error("This Post has a frozen X request. Create another Post to change its orientation or replies.");
|
|
261
|
+
return post;
|
|
262
|
+
}
|
|
263
|
+
if (post.runtime.url)
|
|
264
|
+
return post;
|
|
265
|
+
if (post.runtime.outbound && args.orientation)
|
|
266
|
+
throw new Error("This Post already has delivered media. Create another Post to change orientation.");
|
|
267
|
+
const caption = post.runtime.outbound?.caption ?? [post.definition.caption, post.definition.credits].filter(Boolean).join("\n\n");
|
|
268
|
+
if (!caption.trim())
|
|
269
|
+
throw new Error("A post title is required");
|
|
270
|
+
const limit = await postCharacterLimit(caption, args.followups ?? [], args.store);
|
|
271
|
+
if (caption.length > limit)
|
|
272
|
+
throw characterLimitError("Post title", limit);
|
|
273
|
+
const prepared = await prepareFollowups(args.engine, args.followups ?? [], limit);
|
|
274
|
+
const source = post.definition.source;
|
|
275
|
+
const media = await prepareMedia(args.engine, source.snapshotArtifactId, args.orientation);
|
|
276
|
+
try {
|
|
277
|
+
const outbound = await freezePostOutbound(args.engine, post.id, { bytes: media.buffer, filename: media.tempPath ? "original.mp4" : source.filename });
|
|
278
|
+
const frozen = [];
|
|
279
|
+
const attachments = [];
|
|
280
|
+
for (const [index, followup] of prepared.entries()) {
|
|
281
|
+
const ids = [];
|
|
282
|
+
for (const [imageIndex, image] of followup.media.entries()) {
|
|
283
|
+
const created = await args.engine.artifacts.create("image", `Post reply ${index + 1} image ${imageIndex + 1}`);
|
|
284
|
+
if (!created.ok)
|
|
285
|
+
throw new Error(created.error.message);
|
|
286
|
+
const extension = image.info.mediaType.split("/")[1];
|
|
287
|
+
const filename = `original.${extension}`;
|
|
288
|
+
const written = await args.engine.files.write(created.value.artifactId, filename, image.buffer);
|
|
289
|
+
if (!written.ok)
|
|
290
|
+
throw new Error(written.error.message);
|
|
291
|
+
const manifest = await args.engine.files.manifest(created.value.artifactId);
|
|
292
|
+
if (!manifest.ok)
|
|
293
|
+
throw new Error(manifest.error.message);
|
|
294
|
+
attachments.push({ artifactId: created.value.artifactId, filename, revision: args.engine.head, objectHash: manifest.value.files.find(file => file.name === filename).objectHash });
|
|
295
|
+
ids.push(created.value.artifactId);
|
|
296
|
+
}
|
|
297
|
+
frozen.push({ text: followup.text, image_asset_ids: ids });
|
|
298
|
+
}
|
|
299
|
+
const request = { text: outbound.caption, followups: frozen, sourceFollowups: args.followups ?? [], orientation: args.orientation, attachments };
|
|
300
|
+
const receipt = await args.engine.files.write(outbound.artifactId, "x-request.json", JSON.stringify(request));
|
|
301
|
+
if (!receipt.ok)
|
|
302
|
+
throw new Error(receipt.error.message);
|
|
303
|
+
patchPostRuntime(args.engine, post.id, { xRequest: request });
|
|
304
|
+
return getPost(args.engine, post.id);
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
if (media.tempPath)
|
|
308
|
+
await unlink(media.tempPath).catch(() => undefined);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async function prepareFrozenFollowups(engine, request, limit) {
|
|
312
|
+
if (!request.attachments)
|
|
313
|
+
return prepareFollowups(engine, request.followups, limit);
|
|
314
|
+
return Promise.all(request.followups.map(async (followup) => ({
|
|
315
|
+
text: followup.text,
|
|
316
|
+
media: await Promise.all((followup.image_asset_ids ?? []).map(async (assetId) => {
|
|
317
|
+
const attachment = request.attachments.find(item => item.artifactId === assetId);
|
|
318
|
+
if (!attachment)
|
|
319
|
+
throw new Error("Frozen reply attachment is missing");
|
|
320
|
+
const bytes = await engine.files.readAtRevision(assetId, attachment.filename, attachment.revision);
|
|
321
|
+
if (!bytes.ok)
|
|
322
|
+
throw new Error(bytes.error.message);
|
|
323
|
+
const info = inferMediaType(attachment.filename);
|
|
324
|
+
if (!info)
|
|
325
|
+
throw new Error("Frozen reply attachment has an unsupported media type");
|
|
326
|
+
return { assetId, buffer: bytes.value, info, tempPath: null };
|
|
327
|
+
})),
|
|
328
|
+
})));
|
|
329
|
+
}
|
|
330
|
+
export async function postToX(args) {
|
|
331
|
+
const post = await prepareXPublication(args);
|
|
332
|
+
return withNotebookMutation(`post:${post.id}`, () => publishPreparedPost({ ...args, postCellId: post.id }));
|
|
333
|
+
}
|
|
334
|
+
async function publishPreparedPost(args) {
|
|
335
|
+
const { engine, scheduleId, scheduledFor, signal, postCellId } = args;
|
|
336
|
+
const post = getPost(engine, postCellId);
|
|
337
|
+
const runtime = readPostRuntime(engine, postCellId);
|
|
338
|
+
if (runtime.x && runtime.url && runtime.postedAt)
|
|
339
|
+
return {
|
|
340
|
+
status: runtime.status === "partially_posted" || runtime.status === "posting" ? "partially_posted" : "posted",
|
|
341
|
+
tweet_id: runtime.x.tweetId, tweet_url: runtime.url, media_id: runtime.x.mediaId, posted_at: runtime.postedAt,
|
|
342
|
+
replies: runtime.x.replies, followup_error: runtime.x.followupError ?? null, cell_id: postCellId, cell_address: post.address,
|
|
343
|
+
};
|
|
344
|
+
if (runtime.url)
|
|
345
|
+
throw new Error("This Post already has a published URL");
|
|
346
|
+
if (["posting", "needs_reconciliation"].includes(runtime.status))
|
|
347
|
+
throw new Error("Publication outcome is uncertain. Check X and paste the existing post link. If nothing was published, create a new Post.");
|
|
348
|
+
const assetId = post.definition.source.artifactId;
|
|
349
|
+
const text = runtime.xRequest.text;
|
|
350
|
+
const followups = runtime.xRequest.followups;
|
|
351
|
+
const maxCharacters = await postCharacterLimit(text, followups, args.store);
|
|
352
|
+
const outbound = runtime.outbound;
|
|
353
|
+
const frozenBytes = await engine.files.readAtRevision(outbound.artifactId, outbound.filename, outbound.revision);
|
|
354
|
+
if (!frozenBytes.ok)
|
|
355
|
+
throw new Error(frozenBytes.error.message);
|
|
356
|
+
const rootMedia = { assetId: outbound.artifactId, buffer: frozenBytes.value, info: inferMediaType(outbound.filename), tempPath: null };
|
|
357
|
+
try {
|
|
358
|
+
const preparedFollowups = await prepareFrozenFollowups(engine, runtime.xRequest, maxCharacters);
|
|
359
|
+
const x = await getXClient(engine, args.x);
|
|
360
|
+
const mediaId = await uploadMedia(x, rootMedia.buffer, rootMedia.info, signal);
|
|
361
|
+
const uploadedFollowups = [];
|
|
362
|
+
for (const followup of preparedFollowups) {
|
|
363
|
+
const mediaIds = [];
|
|
364
|
+
for (const media of followup.media) {
|
|
365
|
+
mediaIds.push(await uploadMedia(x, media.buffer, media.info, signal));
|
|
366
|
+
}
|
|
367
|
+
uploadedFollowups.push({
|
|
368
|
+
...(followup.text ? { text: followup.text } : {}),
|
|
369
|
+
mediaIds,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
patchPostRuntime(engine, postCellId, { status: "needs_reconciliation", error: "Publication request is in flight. Check X before retrying after an interruption." });
|
|
373
|
+
const tweetRes = await x.post.v2.tweets({ text, media: { media_ids: [mediaId] } }, signal);
|
|
374
|
+
const tweetId = tweetRes.data.id;
|
|
375
|
+
const postedAt = new Date().toISOString();
|
|
376
|
+
patchPostRuntime(engine, postCellId, { status: uploadedFollowups.length ? "partially_posted" : "posted", url: tweetUrl(tweetId), postedAt, error: null, nextMetricAt: postedAt, x: { tweetId, mediaId, replies: [], followupError: uploadedFollowups.length ? { reply_number: 1, message: "Thread interrupted; root post was published" } : null } });
|
|
377
|
+
const replies = [];
|
|
378
|
+
let parentTweetId = tweetId;
|
|
379
|
+
let followupError = null;
|
|
380
|
+
for (const [index, followup] of uploadedFollowups.entries()) {
|
|
381
|
+
try {
|
|
382
|
+
const replyRes = await x.post.v2.tweets({
|
|
383
|
+
...(followup.text ? { text: followup.text } : {}),
|
|
384
|
+
...(followup.mediaIds.length > 0
|
|
385
|
+
? { media: { media_ids: followup.mediaIds } }
|
|
386
|
+
: {}),
|
|
387
|
+
reply: { in_reply_to_tweet_id: parentTweetId },
|
|
388
|
+
}, signal);
|
|
389
|
+
const replyId = replyRes.data.id;
|
|
390
|
+
const replyResult = {
|
|
391
|
+
tweet_id: replyId,
|
|
392
|
+
tweet_url: tweetUrl(replyId),
|
|
393
|
+
media_ids: followup.mediaIds,
|
|
394
|
+
text: followup.text ?? null,
|
|
395
|
+
posted_at: new Date().toISOString(),
|
|
396
|
+
};
|
|
397
|
+
replies.push(replyResult);
|
|
398
|
+
parentTweetId = replyId;
|
|
399
|
+
patchPostRuntime(engine, postCellId, { x: { tweetId, mediaId, replies: [...replies], followupError: index + 1 < uploadedFollowups.length ? { reply_number: index + 2, message: "Thread interrupted after this reply" } : null } });
|
|
400
|
+
}
|
|
401
|
+
catch (cause) {
|
|
402
|
+
followupError = {
|
|
403
|
+
reply_number: index + 1,
|
|
404
|
+
message: cause instanceof Error ? cause.message : String(cause),
|
|
405
|
+
};
|
|
406
|
+
break;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const status = followupError ? "partially_posted" : "posted";
|
|
410
|
+
patchPostRuntime(engine, postCellId, { status, x: { tweetId, mediaId, replies, followupError }, error: followupError?.message ?? null });
|
|
411
|
+
try {
|
|
412
|
+
await appendXPostMetadata({
|
|
413
|
+
engine,
|
|
414
|
+
assetId,
|
|
415
|
+
entry: {
|
|
416
|
+
tweet_id: tweetId,
|
|
417
|
+
tweet_url: tweetUrl(tweetId),
|
|
418
|
+
posted_at: postedAt,
|
|
419
|
+
media_id: mediaId,
|
|
420
|
+
text,
|
|
421
|
+
scheduled_for: scheduledFor ?? null,
|
|
422
|
+
schedule_id: scheduleId ?? null,
|
|
423
|
+
status,
|
|
424
|
+
replies,
|
|
425
|
+
followup_error: followupError,
|
|
426
|
+
},
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
catch (err) {
|
|
430
|
+
logger.error("post-record-failed", { error: err });
|
|
431
|
+
}
|
|
432
|
+
const posted = {
|
|
433
|
+
status,
|
|
434
|
+
tweet_id: tweetId,
|
|
435
|
+
tweet_url: tweetUrl(tweetId),
|
|
436
|
+
media_id: mediaId,
|
|
437
|
+
posted_at: postedAt,
|
|
438
|
+
replies,
|
|
439
|
+
followup_error: followupError,
|
|
440
|
+
};
|
|
441
|
+
posted.cell_id = postCellId;
|
|
442
|
+
posted.cell_address = post.address;
|
|
443
|
+
return posted;
|
|
444
|
+
}
|
|
445
|
+
catch (error) {
|
|
446
|
+
const current = readPostRuntime(engine, postCellId);
|
|
447
|
+
if (!current.url && (current.status !== "needs_reconciliation" || (error instanceof XError && [400, 401, 403, 404, 422, 429].includes(error.status))))
|
|
448
|
+
patchPostRuntime(engine, postCellId, { status: "failed", error: error instanceof Error ? error.message : "Publication failed" });
|
|
449
|
+
throw error;
|
|
450
|
+
}
|
|
451
|
+
finally {
|
|
452
|
+
if (rootMedia.tempPath) {
|
|
453
|
+
await unlink(rootMedia.tempPath).catch(() => undefined);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { notebookOccupiedSlots } from "videobook-engine/notebook-grid";
|
|
2
|
+
import { nextHorizontalSlotFrom, notebookGridTag, } from "videobook-engine/notebook-grid";
|
|
3
|
+
import { mutateNotebook } from "../notebook-mutations.js";
|
|
4
|
+
import { slotRight } from "../notebook-placement.js";
|
|
5
|
+
import { PUBLISHED_POST_SOURCE_OPERATION, PUBLISHED_POST_TARGET_INPUT, sourceCellForPostedArtifact, } from "../notebook-published-post.js";
|
|
6
|
+
function publishedCellType(source) {
|
|
7
|
+
if (source.type === "image" || source.type === "video")
|
|
8
|
+
return source.type;
|
|
9
|
+
return "video";
|
|
10
|
+
}
|
|
11
|
+
function publishedPostInputs(posted, sourceCellId) {
|
|
12
|
+
return {
|
|
13
|
+
sourceOperation: PUBLISHED_POST_SOURCE_OPERATION,
|
|
14
|
+
platform: "x",
|
|
15
|
+
tweetId: posted.tweet_id,
|
|
16
|
+
tweetUrl: posted.tweet_url,
|
|
17
|
+
postedAt: posted.posted_at,
|
|
18
|
+
status: posted.status,
|
|
19
|
+
replies: posted.replies.map((reply) => ({
|
|
20
|
+
tweetId: reply.tweet_id,
|
|
21
|
+
tweetUrl: reply.tweet_url,
|
|
22
|
+
})),
|
|
23
|
+
sourceCellId,
|
|
24
|
+
...(posted.followup_error
|
|
25
|
+
? {
|
|
26
|
+
followupError: {
|
|
27
|
+
replyNumber: posted.followup_error.reply_number,
|
|
28
|
+
message: posted.followup_error.message,
|
|
29
|
+
},
|
|
30
|
+
}
|
|
31
|
+
: {}),
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function locatePublishedPostAnchor(engine, artifactId) {
|
|
35
|
+
for (const listed of engine.notebooks.list()) {
|
|
36
|
+
const read = engine.notebooks.read(listed.id);
|
|
37
|
+
if (!read.ok)
|
|
38
|
+
continue;
|
|
39
|
+
const source = sourceCellForPostedArtifact(read.value, artifactId);
|
|
40
|
+
if (source)
|
|
41
|
+
return { notebookId: listed.id, source };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
export async function createPublishedPostCell(args) {
|
|
46
|
+
const located = locatePublishedPostAnchor(args.engine, args.assetId);
|
|
47
|
+
if (!located)
|
|
48
|
+
return null;
|
|
49
|
+
const placement = { cell: null };
|
|
50
|
+
const written = await mutateNotebook(args.engine, located.notebookId, "post_to_x", (notebook) => {
|
|
51
|
+
const anchor = notebook.cells.find((cell) => cell.id === located.source.id)
|
|
52
|
+
?? sourceCellForPostedArtifact(notebook, args.assetId);
|
|
53
|
+
if (!anchor) {
|
|
54
|
+
throw new Error(`Source cell not found for posted asset ${args.assetId}`);
|
|
55
|
+
}
|
|
56
|
+
const occupied = notebookOccupiedSlots(notebook.cells);
|
|
57
|
+
const slot = nextHorizontalSlotFrom(slotRight(anchor.slot), occupied);
|
|
58
|
+
const cell = args.engine.notebooks.createCell({
|
|
59
|
+
type: publishedCellType(anchor),
|
|
60
|
+
label: "Posted to X",
|
|
61
|
+
slot,
|
|
62
|
+
inputs: publishedPostInputs(args.posted, anchor.id),
|
|
63
|
+
outputArtifactId: args.assetId,
|
|
64
|
+
});
|
|
65
|
+
const edge = args.engine.notebooks.createEdge({
|
|
66
|
+
source: anchor.id,
|
|
67
|
+
target: cell.id,
|
|
68
|
+
targetInput: PUBLISHED_POST_TARGET_INPUT,
|
|
69
|
+
});
|
|
70
|
+
placement.cell = cell;
|
|
71
|
+
return {
|
|
72
|
+
...notebook,
|
|
73
|
+
cells: [...notebook.cells, cell],
|
|
74
|
+
edges: [...notebook.edges, edge],
|
|
75
|
+
};
|
|
76
|
+
}, {
|
|
77
|
+
sourceCellId: located.source.id,
|
|
78
|
+
tweetId: args.posted.tweet_id,
|
|
79
|
+
status: args.posted.status,
|
|
80
|
+
});
|
|
81
|
+
if (!written.ok)
|
|
82
|
+
throw new Error(written.error.message);
|
|
83
|
+
const cell = placement.cell;
|
|
84
|
+
if (!cell)
|
|
85
|
+
throw new Error("Published post cell was not created");
|
|
86
|
+
return {
|
|
87
|
+
cellId: cell.id,
|
|
88
|
+
cellAddress: notebookGridTag(cell.slot),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createXOAuth, XError } from "@apicity/x";
|
|
2
|
+
import { appState } from "../app-state.js";
|
|
3
|
+
const REFRESH_BUFFER_MS = 60_000;
|
|
4
|
+
const SETTINGS_KEY = "x.oauth.tokens";
|
|
5
|
+
let writeChain = Promise.resolve();
|
|
6
|
+
function serialize(fn) {
|
|
7
|
+
const next = writeChain.then(fn, fn);
|
|
8
|
+
writeChain = next.catch(() => undefined);
|
|
9
|
+
return next;
|
|
10
|
+
}
|
|
11
|
+
export async function readTokens(store = appState) {
|
|
12
|
+
const parsed = await store.get(SETTINGS_KEY);
|
|
13
|
+
if (parsed
|
|
14
|
+
&& typeof parsed.access_token === "string"
|
|
15
|
+
&& typeof parsed.refresh_token === "string"
|
|
16
|
+
&& typeof parsed.expires_at === "string") {
|
|
17
|
+
return parsed;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
export async function writeTokens(state, store = appState) {
|
|
22
|
+
await serialize(() => store.set(SETTINGS_KEY, state));
|
|
23
|
+
}
|
|
24
|
+
function expiresWithin(state, ms) {
|
|
25
|
+
const expiresAt = new Date(state.expires_at).getTime();
|
|
26
|
+
return Number.isNaN(expiresAt) || expiresAt - Date.now() < ms;
|
|
27
|
+
}
|
|
28
|
+
export async function refreshTokens(state, options = {}) {
|
|
29
|
+
const clientId = process.env.X_CLIENT_ID;
|
|
30
|
+
const clientSecret = process.env.X_CLIENT_SECRET;
|
|
31
|
+
if (!clientId || !clientSecret) {
|
|
32
|
+
throw new Error("X token expired and X_CLIENT_ID/X_CLIENT_SECRET are not configured");
|
|
33
|
+
}
|
|
34
|
+
const oauth = createXOAuth({ clientId, clientSecret, fetch: options.fetchImpl ?? fetch });
|
|
35
|
+
let body;
|
|
36
|
+
try {
|
|
37
|
+
body = await oauth.post.v2.oauth2.token({
|
|
38
|
+
grant_type: "refresh_token",
|
|
39
|
+
refresh_token: state.refresh_token,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
if (error instanceof XError) {
|
|
44
|
+
const detail = error.body ? JSON.stringify(error.body) : error.message;
|
|
45
|
+
throw new Error(`X token refresh failed (${error.status}): ${detail}`);
|
|
46
|
+
}
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
const next = {
|
|
50
|
+
access_token: body.access_token,
|
|
51
|
+
refresh_token: body.refresh_token ?? state.refresh_token,
|
|
52
|
+
expires_at: new Date(Date.now() + body.expires_in * 1000).toISOString(),
|
|
53
|
+
};
|
|
54
|
+
await writeTokens(next, options.store ?? appState);
|
|
55
|
+
return next;
|
|
56
|
+
}
|
|
57
|
+
export async function getValidAccessToken(options = {}) {
|
|
58
|
+
const store = options.store ?? appState;
|
|
59
|
+
const state = await readTokens(store);
|
|
60
|
+
if (state) {
|
|
61
|
+
if (!expiresWithin(state, REFRESH_BUFFER_MS))
|
|
62
|
+
return state.access_token;
|
|
63
|
+
return (await refreshTokens(state, { fetchImpl: options.fetchImpl, store })).access_token;
|
|
64
|
+
}
|
|
65
|
+
const fallback = process.env.X_ACCESS_TOKEN;
|
|
66
|
+
if (fallback)
|
|
67
|
+
return fallback;
|
|
68
|
+
throw new Error("No X access token is configured. Set X_ACCESS_TOKEN or connect X OAuth.");
|
|
69
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { accessSync, constants, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { delimiter, isAbsolute, join } from "node:path";
|
|
4
|
+
import { FFMPEG } from "./ffmpeg-binary.js";
|
|
5
|
+
const YTDLP_OVERRIDE = "VIDEOBOOK_YTDLP_PATH";
|
|
6
|
+
const require = createRequire(import.meta.url);
|
|
7
|
+
export const YTDLP_MISSING_MESSAGE = "yt-dlp is not installed. Put yt-dlp on PATH (for example brew install yt-dlp), set VIDEOBOOK_YTDLP_PATH, "
|
|
8
|
+
+ "or reinstall with npm install -g videobook --allow-scripts=ffmpeg-static,ytdlp-nodejs,@dolthub/doltlite,usearch,onnxruntime-node,esbuild";
|
|
9
|
+
function isExecutableFile(path) {
|
|
10
|
+
try {
|
|
11
|
+
if (!statSync(path).isFile())
|
|
12
|
+
return false;
|
|
13
|
+
accessSync(path, constants.X_OK);
|
|
14
|
+
return true;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function executableNames(name, env) {
|
|
21
|
+
if (process.platform !== "win32")
|
|
22
|
+
return [name];
|
|
23
|
+
const extensions = (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM").split(";").filter(Boolean);
|
|
24
|
+
return [name, ...extensions.map((extension) => `${name}${extension.toLowerCase()}`)];
|
|
25
|
+
}
|
|
26
|
+
export function findOnPath(name, env = process.env) {
|
|
27
|
+
const directories = (env.PATH ?? "").split(delimiter).filter((directory) => isAbsolute(directory));
|
|
28
|
+
for (const directory of directories) {
|
|
29
|
+
const match = executableNames(name, env).map((candidate) => join(directory, candidate)).find(isExecutableFile);
|
|
30
|
+
if (match)
|
|
31
|
+
return match;
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
export function bundledYtdlp() {
|
|
36
|
+
try {
|
|
37
|
+
const { helpers } = require("ytdlp-nodejs");
|
|
38
|
+
const path = helpers.findYtdlpBinary();
|
|
39
|
+
return path && isExecutableFile(path) ? path : null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export function resolveYtdlp(env = process.env, bundled = bundledYtdlp()) {
|
|
46
|
+
const override = env[YTDLP_OVERRIDE]?.trim();
|
|
47
|
+
if (override)
|
|
48
|
+
return { path: override, source: "VIDEOBOOK_YTDLP_PATH" };
|
|
49
|
+
if (bundled)
|
|
50
|
+
return { path: bundled, source: "bundled" };
|
|
51
|
+
const onPath = findOnPath("yt-dlp", env);
|
|
52
|
+
return onPath ? { path: onPath, source: "PATH" } : null;
|
|
53
|
+
}
|
|
54
|
+
function ffmpegForYtdlp(env) {
|
|
55
|
+
if (isAbsolute(FFMPEG))
|
|
56
|
+
return isExecutableFile(FFMPEG) ? FFMPEG : null;
|
|
57
|
+
return findOnPath(FFMPEG, env);
|
|
58
|
+
}
|
|
59
|
+
export function ytdlpRunOptions(env = process.env, bundled = bundledYtdlp()) {
|
|
60
|
+
const binary = resolveYtdlp(env, bundled);
|
|
61
|
+
if (!binary)
|
|
62
|
+
throw new Error(YTDLP_MISSING_MESSAGE);
|
|
63
|
+
const ffmpeg = ffmpegForYtdlp(env);
|
|
64
|
+
return { binaryPath: binary.path, ...(ffmpeg ? { ffmpegPath: ffmpeg } : {}) };
|
|
65
|
+
}
|