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,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
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
const SCRIPT = `ObjC.import("Foundation");
|
|
3
|
+
function run(argv) {
|
|
4
|
+
return JSON.stringify(argv.map(function (path) {
|
|
5
|
+
var url = $.NSURL.fileURLWithPath(path);
|
|
6
|
+
function get(key) { var out = Ref(); if (!url.getResourceValueForKeyError(out, key, null)) return null; var v = out[0]; return v === undefined || v === null || (v.isNil && v.isNil()) ? null : ObjC.unwrap(v); }
|
|
7
|
+
var err = get($.NSURLUbiquitousItemUploadingErrorKey);
|
|
8
|
+
return { path: path, exists: $.NSFileManager.defaultManager.fileExistsAtPath(path), ubiquitous: get($.NSURLIsUbiquitousItemKey), shared: get($.NSURLUbiquitousItemIsSharedKey), role: get($.NSURLUbiquitousSharedItemCurrentUserRoleKey), uploaded: get($.NSURLUbiquitousItemIsUploadedKey), uploading: get($.NSURLUbiquitousItemIsUploadingKey), error: err ? ObjC.unwrap(err.localizedDescription) : null };
|
|
9
|
+
}));
|
|
10
|
+
}`;
|
|
11
|
+
function flag(value) {
|
|
12
|
+
return typeof value === "boolean" ? value : typeof value === "number" ? value !== 0 : null;
|
|
13
|
+
}
|
|
14
|
+
function role(value) {
|
|
15
|
+
if (typeof value !== "string")
|
|
16
|
+
return null;
|
|
17
|
+
if (value.endsWith("Owner"))
|
|
18
|
+
return "owner";
|
|
19
|
+
if (value.endsWith("Participant"))
|
|
20
|
+
return "participant";
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
export function parseUbiquitousStates(output) {
|
|
24
|
+
const parsed = JSON.parse(output);
|
|
25
|
+
if (!Array.isArray(parsed))
|
|
26
|
+
throw new Error("Unexpected iCloud status output");
|
|
27
|
+
return parsed.map((item) => {
|
|
28
|
+
const record = item && typeof item === "object" ? item : {};
|
|
29
|
+
const ubiquitous = flag(record.ubiquitous) === true;
|
|
30
|
+
return {
|
|
31
|
+
path: typeof record.path === "string" ? record.path : "",
|
|
32
|
+
exists: record.exists === true,
|
|
33
|
+
ubiquitous: flag(record.ubiquitous),
|
|
34
|
+
shared: ubiquitous ? flag(record.shared) ?? false : null,
|
|
35
|
+
role: ubiquitous ? role(record.role) : null,
|
|
36
|
+
uploaded: ubiquitous ? flag(record.uploaded) : null,
|
|
37
|
+
uploading: ubiquitous ? flag(record.uploading) : null,
|
|
38
|
+
error: typeof record.error === "string" && record.error ? record.error : null,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function summarizeUbiquitousSync(states) {
|
|
43
|
+
if (!states || states.length === 0 || states.some((state) => state.ubiquitous !== true))
|
|
44
|
+
return { phase: "not-icloud", error: null };
|
|
45
|
+
const error = states.find((state) => state.error)?.error ?? null;
|
|
46
|
+
if (error)
|
|
47
|
+
return { phase: "error", error };
|
|
48
|
+
return states.every((state) => state.uploaded === true && state.uploading !== true)
|
|
49
|
+
? { phase: "uploaded", error: null }
|
|
50
|
+
: { phase: "uploading", error: null };
|
|
51
|
+
}
|
|
52
|
+
export async function readUbiquitousStates(paths) {
|
|
53
|
+
if (process.platform !== "darwin" || paths.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const output = await new Promise((resolve, reject) => {
|
|
57
|
+
execFile("osascript", ["-l", "JavaScript", "-e", SCRIPT, ...paths], { timeout: 10_000 }, (error, stdout) => error ? reject(error) : resolve(stdout));
|
|
58
|
+
});
|
|
59
|
+
return parseUbiquitousStates(output);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export const POST_COPY_LANGUAGES = ["en", "th"];
|
|
2
|
+
export const POST_COPY_LANGUAGE_NAMES = { en: "English", th: "Thai" };
|
|
3
|
+
export const PLATFORM_CAPTION_LIMITS = { x: 280, instagram: 2200, tiktok: 2200, youtube: 5000, other: 2200 };
|
|
4
|
+
export const PLATFORM_TAG_LIMITS = { x: 2, instagram: 10, tiktok: 5, youtube: 15, other: 8 };
|
|
5
|
+
export const PLATFORM_NAMES = { x: "X", instagram: "Instagram", tiktok: "TikTok", youtube: "YouTube Shorts", other: "Other" };
|
|
6
|
+
const TAG_TOKEN = /^#[\p{L}\p{M}\p{N}_]+$/u;
|
|
7
|
+
function normalizePostTag(value) {
|
|
8
|
+
return value.trim().replace(/^#+/u, "").replace(/[^\p{L}\p{M}\p{N}_]/gu, "");
|
|
9
|
+
}
|
|
10
|
+
export function normalizePostTags(values, limit = Number.POSITIVE_INFINITY) {
|
|
11
|
+
const seen = new Set();
|
|
12
|
+
const tags = [];
|
|
13
|
+
for (const value of values) {
|
|
14
|
+
const tag = normalizePostTag(value);
|
|
15
|
+
const key = tag.toLocaleLowerCase();
|
|
16
|
+
if (!tag || seen.has(key))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(key);
|
|
19
|
+
tags.push(tag);
|
|
20
|
+
if (tags.length >= limit)
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
return tags;
|
|
24
|
+
}
|
|
25
|
+
function isTagLine(line) {
|
|
26
|
+
const tokens = line.trim().split(/\s+/u);
|
|
27
|
+
return tokens.length > 0 && tokens.every((token) => TAG_TOKEN.test(token));
|
|
28
|
+
}
|
|
29
|
+
export function splitCaptionTags(caption) {
|
|
30
|
+
const trimmed = caption.trimEnd();
|
|
31
|
+
const lines = trimmed.split("\n");
|
|
32
|
+
const last = lines[lines.length - 1] ?? "";
|
|
33
|
+
if (!last.trim() || !isTagLine(last))
|
|
34
|
+
return { body: caption, tags: [] };
|
|
35
|
+
return { body: lines.slice(0, -1).join("\n").trimEnd(), tags: normalizePostTags(last.trim().split(/\s+/u)) };
|
|
36
|
+
}
|
|
37
|
+
export function joinCaptionTags(body, tags) {
|
|
38
|
+
const line = normalizePostTags(tags).map((tag) => `#${tag}`).join(" ");
|
|
39
|
+
const text = body.trimEnd();
|
|
40
|
+
if (!line)
|
|
41
|
+
return text;
|
|
42
|
+
return text ? `${text}\n\n${line}` : line;
|
|
43
|
+
}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { chatIntelligenceRequestOptions } from "../chat-models.js";
|
|
2
|
+
import { LIGHTNING_POST_TAGS } from "../lightning-defaults.js";
|
|
3
|
+
import { buildIntelligenceResponses, firstResponseText, intelligenceRequest } from "../openai-intelligence.js";
|
|
4
|
+
import { readAssetTranscript } from "../transcript-projection.js";
|
|
5
|
+
import { isAnalyzableVideo, VIDEO_ANALYSIS_FILE } from "../video-analysis-projection.js";
|
|
6
|
+
import { normalizePostTags, PLATFORM_CAPTION_LIMITS, PLATFORM_NAMES, PLATFORM_TAG_LIMITS, POST_COPY_LANGUAGE_NAMES, } from "./post-copy-format.js";
|
|
7
|
+
const MAX_TRANSCRIPT_CHARS = 4000;
|
|
8
|
+
const MAX_PROMPT_CHARS = 1000;
|
|
9
|
+
const MAX_SCENES = 12;
|
|
10
|
+
const POST_COPY_JSON_SCHEMA = {
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
required: ["caption", "tags"],
|
|
14
|
+
properties: {
|
|
15
|
+
caption: { type: "string" },
|
|
16
|
+
tags: { type: "array", items: { type: "string" } },
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
function text(value) {
|
|
20
|
+
return typeof value === "string" ? value.trim() : "";
|
|
21
|
+
}
|
|
22
|
+
function texts(value) {
|
|
23
|
+
return Array.isArray(value) ? value.map(text).filter(Boolean) : [];
|
|
24
|
+
}
|
|
25
|
+
function sceneLines(value) {
|
|
26
|
+
if (!Array.isArray(value))
|
|
27
|
+
return [];
|
|
28
|
+
return value.slice(0, MAX_SCENES).map((scene) => {
|
|
29
|
+
const record = scene && typeof scene === "object" ? scene : {};
|
|
30
|
+
const at = typeof record.timestamp === "number" ? `${record.timestamp.toFixed(1)}s: ` : "";
|
|
31
|
+
const description = text(record.description);
|
|
32
|
+
return description ? `${at}${description}` : "";
|
|
33
|
+
}).filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
function postCopyAnalysisFrom(raw) {
|
|
36
|
+
if (!raw || typeof raw !== "object")
|
|
37
|
+
return null;
|
|
38
|
+
const record = raw;
|
|
39
|
+
const analysis = {
|
|
40
|
+
summary: text(record.summary),
|
|
41
|
+
mood: text(record.mood),
|
|
42
|
+
setting: text(record.setting),
|
|
43
|
+
subjects: texts(record.subjects),
|
|
44
|
+
actions: texts(record.actions),
|
|
45
|
+
tags: texts(record.tags),
|
|
46
|
+
scenes: sceneLines(record.scenes),
|
|
47
|
+
textOnScreen: texts(record.textOnScreen),
|
|
48
|
+
};
|
|
49
|
+
const empty = !analysis.summary && analysis.scenes.length === 0 && analysis.tags.length === 0 && analysis.subjects.length === 0;
|
|
50
|
+
return empty ? null : analysis;
|
|
51
|
+
}
|
|
52
|
+
async function readAnalysis(engine, assetId) {
|
|
53
|
+
const read = await engine.files.read(assetId, VIDEO_ANALYSIS_FILE);
|
|
54
|
+
if (!read.ok)
|
|
55
|
+
return null;
|
|
56
|
+
try {
|
|
57
|
+
return postCopyAnalysisFrom(JSON.parse(read.value.toString()));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function sourceArtifactIds(post) {
|
|
64
|
+
const source = post.definition.source;
|
|
65
|
+
if (!source)
|
|
66
|
+
return [];
|
|
67
|
+
return [...new Set([source.artifactId, source.snapshotArtifactId].filter((id) => Boolean(id)))];
|
|
68
|
+
}
|
|
69
|
+
export async function postAnalysisTarget(engine, post) {
|
|
70
|
+
const ids = sourceArtifactIds(post);
|
|
71
|
+
for (const assetId of ids) {
|
|
72
|
+
if (await readAnalysis(engine, assetId))
|
|
73
|
+
return { assetId, analyzable: true, analyzed: true };
|
|
74
|
+
}
|
|
75
|
+
const assetId = ids[0];
|
|
76
|
+
if (!assetId)
|
|
77
|
+
return null;
|
|
78
|
+
const artifact = engine.artifacts.get(assetId);
|
|
79
|
+
return { assetId, analyzable: artifact.ok && isAnalyzableVideo(artifact.value.kind), analyzed: false };
|
|
80
|
+
}
|
|
81
|
+
async function readTranscriptText(engine, assetIds) {
|
|
82
|
+
for (const assetId of assetIds) {
|
|
83
|
+
const transcript = await readAssetTranscript(engine, assetId);
|
|
84
|
+
if (transcript.kind === "timed" || transcript.kind === "plain") {
|
|
85
|
+
const value = transcript.text.trim();
|
|
86
|
+
if (value)
|
|
87
|
+
return value.slice(0, MAX_TRANSCRIPT_CHARS);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
export async function readPostCopyContext(engine, post) {
|
|
93
|
+
const ids = sourceArtifactIds(post);
|
|
94
|
+
let analysis = null;
|
|
95
|
+
for (const assetId of ids) {
|
|
96
|
+
analysis = await readAnalysis(engine, assetId);
|
|
97
|
+
if (analysis)
|
|
98
|
+
break;
|
|
99
|
+
}
|
|
100
|
+
const prompts = post.definition.lineage
|
|
101
|
+
.map((step) => text(step.prompt).slice(0, MAX_PROMPT_CHARS))
|
|
102
|
+
.filter(Boolean)
|
|
103
|
+
.slice(-2);
|
|
104
|
+
const label = post.definition.lineage.find((step) => step.cellId === post.definition.source?.cellId)?.label ?? post.label;
|
|
105
|
+
return { label, analysis, transcript: await readTranscriptText(engine, ids), prompts };
|
|
106
|
+
}
|
|
107
|
+
function listLine(name, values) {
|
|
108
|
+
return values.length ? `${name}: ${values.join(", ")}` : null;
|
|
109
|
+
}
|
|
110
|
+
function analysisSection(analysis) {
|
|
111
|
+
if (!analysis)
|
|
112
|
+
return "VIDEO ANALYSIS: none available. Rely only on the clip label and prompts below.";
|
|
113
|
+
const lines = [
|
|
114
|
+
analysis.summary ? `Summary: ${analysis.summary}` : null,
|
|
115
|
+
analysis.mood ? `Mood: ${analysis.mood}` : null,
|
|
116
|
+
analysis.setting ? `Setting: ${analysis.setting}` : null,
|
|
117
|
+
listLine("Subjects", analysis.subjects),
|
|
118
|
+
listLine("Actions", analysis.actions),
|
|
119
|
+
listLine("Analysis tags", analysis.tags),
|
|
120
|
+
listLine("Text on screen", analysis.textOnScreen),
|
|
121
|
+
analysis.scenes.length ? `Scenes:\n${analysis.scenes.map((scene) => `- ${scene}`).join("\n")}` : null,
|
|
122
|
+
].filter((line) => line !== null);
|
|
123
|
+
return `VIDEO ANALYSIS:\n${lines.join("\n")}`;
|
|
124
|
+
}
|
|
125
|
+
function rewriteSection(request) {
|
|
126
|
+
const caption = request.caption?.trim();
|
|
127
|
+
const tags = request.tags?.length ? request.tags.map((tag) => `#${tag}`).join(" ") : "";
|
|
128
|
+
if (!caption && !tags)
|
|
129
|
+
return "";
|
|
130
|
+
return `\nCURRENT DRAFT (rewrite it; keep what already works):\nCaption: ${caption || "(empty)"}\nTags: ${tags || "(none)"}\n`;
|
|
131
|
+
}
|
|
132
|
+
export function buildPostCopyPrompt(context, request) {
|
|
133
|
+
const platform = PLATFORM_NAMES[request.platform];
|
|
134
|
+
const language = POST_COPY_LANGUAGE_NAMES[request.language];
|
|
135
|
+
const limit = PLATFORM_CAPTION_LIMITS[request.platform];
|
|
136
|
+
const maxTags = PLATFORM_TAG_LIMITS[request.platform];
|
|
137
|
+
const instruction = request.instruction?.trim();
|
|
138
|
+
return [
|
|
139
|
+
`Write the social post copy for one short video clip that will be posted on ${platform}.`,
|
|
140
|
+
"",
|
|
141
|
+
"RULES:",
|
|
142
|
+
`- Write the caption in ${language}${request.language === "th" ? ", natural and conversational, the way a Thai creator writes" : ""}.`,
|
|
143
|
+
"- Ground every claim in the video analysis, transcript and prompts below. Never invent names, places, events or facts that are not there.",
|
|
144
|
+
"- By default the caption is a short title of one or two words plus one or two emoji, like \"Golden hour 🌅✨\". Write longer copy only when the instruction asks for it.",
|
|
145
|
+
"- The title describes this clip. Do not reuse another video's caption.",
|
|
146
|
+
"- Keep the voice human; no clichés like \"you won't believe\".",
|
|
147
|
+
"- The caption holds no hashtags. Put hashtags in tags, without the # sign and without spaces.",
|
|
148
|
+
`- Return at most ${maxTags} tags. Tags may be ${language}${request.language === "en" ? "" : " or English"} words that people on ${platform} search for.`,
|
|
149
|
+
request.platform === "tiktok" && request.tags === undefined
|
|
150
|
+
? "- Include the tags fyp and bkk unless the creator already supplied tags."
|
|
151
|
+
: "",
|
|
152
|
+
`- The caption plus its tags must stay under ${limit} characters.`,
|
|
153
|
+
instruction ? `\nINSTRUCTION FROM THE CREATOR: ${instruction}` : "",
|
|
154
|
+
rewriteSection(request),
|
|
155
|
+
`CLIP LABEL: ${context.label}`,
|
|
156
|
+
analysisSection(context.analysis),
|
|
157
|
+
context.transcript ? `TRANSCRIPT:\n${context.transcript}` : "TRANSCRIPT: none",
|
|
158
|
+
context.prompts.length ? `HOW THE CLIP WAS MADE (generation prompts):\n${context.prompts.map((prompt) => `- ${prompt}`).join("\n")}` : "",
|
|
159
|
+
].filter((line) => line !== "").join("\n");
|
|
160
|
+
}
|
|
161
|
+
function parsePostCopy(raw, request) {
|
|
162
|
+
let parsed;
|
|
163
|
+
try {
|
|
164
|
+
parsed = JSON.parse(raw);
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
throw new Error("Astra returned post copy that is not JSON.");
|
|
168
|
+
}
|
|
169
|
+
const record = parsed && typeof parsed === "object" ? parsed : {};
|
|
170
|
+
const caption = text(record.caption);
|
|
171
|
+
if (!caption)
|
|
172
|
+
throw new Error("Astra returned an empty caption.");
|
|
173
|
+
const limit = PLATFORM_TAG_LIMITS[request.platform];
|
|
174
|
+
const parsedTags = normalizePostTags(texts(record.tags), limit);
|
|
175
|
+
const tags = request.platform === "tiktok" && request.tags === undefined
|
|
176
|
+
? normalizePostTags([...LIGHTNING_POST_TAGS, ...parsedTags], limit)
|
|
177
|
+
: parsedTags;
|
|
178
|
+
return { caption, tags };
|
|
179
|
+
}
|
|
180
|
+
export function postCopyResponses(apiKey, options = {}) {
|
|
181
|
+
const astra = chatIntelligenceRequestOptions();
|
|
182
|
+
return buildIntelligenceResponses(apiKey, { timeout: 120_000, model: astra.model, reasoningEffort: astra.reasoningEffort, ...options });
|
|
183
|
+
}
|
|
184
|
+
export async function draftPostCopy(responses, context, request, signal) {
|
|
185
|
+
const options = chatIntelligenceRequestOptions();
|
|
186
|
+
const response = await responses(intelligenceRequest({
|
|
187
|
+
input: [{ role: "user", content: buildPostCopyPrompt(context, request) }],
|
|
188
|
+
text: { format: { type: "json_schema", name: "post_copy", schema: POST_COPY_JSON_SCHEMA, strict: true } },
|
|
189
|
+
max_output_tokens: 4096,
|
|
190
|
+
}, options), signal);
|
|
191
|
+
const copy = parsePostCopy(firstResponseText(response), request);
|
|
192
|
+
return { ...copy, platform: request.platform, language: request.language, model: options.model, analyzed: context.analysis !== null };
|
|
193
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
function folderSegment(value) {
|
|
2
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/gu, "-").replace(/^-+|-+$/gu, "") || "book";
|
|
3
|
+
}
|
|
4
|
+
export function defaultPostFolderName(bookName, address, postId) {
|
|
5
|
+
return `${folderSegment(bookName)}/post-${address.replace(/^@/u, "")}-${postId.slice(-6)}`;
|
|
6
|
+
}
|
|
7
|
+
export function icloudFolderError(folder) {
|
|
8
|
+
const trimmed = folder.trim().replace(/\/+$/u, "");
|
|
9
|
+
if (!trimmed)
|
|
10
|
+
return "Choose an iCloud folder.";
|
|
11
|
+
if (trimmed.startsWith("/") || trimmed.startsWith("~"))
|
|
12
|
+
return "Use a folder inside iCloud Drive › Videobook, like my-book/post-a1.";
|
|
13
|
+
if ([...trimmed].some(character => character.charCodeAt(0) < 32 || character.charCodeAt(0) === 127))
|
|
14
|
+
return "Use an iCloud folder without control characters.";
|
|
15
|
+
if (trimmed.split("/").some(segment => segment === ".." || segment === "." || segment === ""))
|
|
16
|
+
return "Use an iCloud folder without empty, . or .. segments.";
|
|
17
|
+
return null;
|
|
18
|
+
}
|