videobook 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +18 -0
- package/LICENSE +21 -0
- package/README.md +133 -2
- package/bin/videobook.js +7 -0
- package/dist/acestep/client.js +203 -0
- package/dist/acestep/config.js +62 -0
- package/dist/acestep/errors.js +8 -0
- package/dist/acestep/process.js +175 -0
- package/dist/alive-captions/analyze.js +117 -0
- package/dist/alive-captions/audio.js +179 -0
- package/dist/alive-captions/frames.js +59 -0
- package/dist/alive-captions/model.js +37 -0
- package/dist/alive-captions/schema.js +49 -0
- package/dist/alive-captions/track.js +9 -0
- package/dist/api-key.js +33 -0
- package/dist/app-state.js +56 -0
- package/dist/app.js +257 -0
- package/dist/archive-b2.js +332 -0
- package/dist/archive-store.js +169 -0
- package/dist/artifact-label.js +36 -0
- package/dist/artifact-refs.js +49 -0
- package/dist/asset-tag-extraction.js +169 -0
- package/dist/asset-tags.js +228 -0
- package/dist/asset-usage.js +23 -0
- package/dist/audio-cue.js +71 -0
- package/dist/audio-encoding.js +32 -0
- package/dist/audio-sync-request.js +1 -0
- package/dist/audio-sync.js +223 -0
- package/dist/audio-trim-range.js +31 -0
- package/dist/book-context.js +31 -0
- package/dist/book-manager.js +374 -0
- package/dist/catalog-gc.js +26 -0
- package/dist/chain-executor.js +815 -0
- package/dist/chain-generation.js +115 -0
- package/dist/chain-plan.js +746 -0
- package/dist/chain-step-review.js +60 -0
- package/dist/character-name.js +25 -0
- package/dist/chat-generation-routing.js +73 -0
- package/dist/chat-generation-settings.js +78 -0
- package/dist/chat-models.js +44 -0
- package/dist/chat-prompt-rewrite.js +67 -0
- package/dist/chat-router.js +628 -0
- package/dist/chat-view-actions.js +49 -0
- package/dist/cli/args.js +192 -0
- package/dist/cli/doctor-command.js +255 -0
- package/dist/cli/entry.js +2 -0
- package/dist/cli/main.js +91 -0
- package/dist/cli/mcp-command.js +82 -0
- package/dist/cli/mcp-relay.js +51 -0
- package/dist/cli/open-browser.js +14 -0
- package/dist/cli/run-command.js +187 -0
- package/dist/cli/serve-command.js +57 -0
- package/dist/cli/stdio-guard.js +8 -0
- package/dist/cold-open-model.js +7 -0
- package/dist/cold-open.js +79 -0
- package/dist/command-reference.js +95 -0
- package/dist/content-store.js +74 -0
- package/dist/cost.js +8 -0
- package/dist/crop-geometry.js +23 -0
- package/dist/dancing-workflow.js +446 -0
- package/dist/data-home.js +44 -0
- package/dist/diagnostics.js +109 -0
- package/dist/dialog-quality.js +34 -0
- package/dist/downloads-dest.js +56 -0
- package/dist/editing-moments.js +194 -0
- package/dist/elevenlabs-audio-extension.js +66 -0
- package/dist/engine.js +54 -0
- package/dist/env-secrets.js +184 -0
- package/dist/ffmpeg-binary.js +12 -0
- package/dist/ffmpeg.js +425 -0
- package/dist/ffprobe-binary.js +55 -0
- package/dist/forget-asset.js +216 -0
- package/dist/frame-utils.js +27 -0
- package/dist/generation-credit-error.js +11 -0
- package/dist/generation-input-mentions.js +117 -0
- package/dist/generation-input-policy.js +68 -0
- package/dist/generation-reference-mentions.js +97 -0
- package/dist/goal-characters.js +49 -0
- package/dist/goal-plan-v3.js +273 -0
- package/dist/goal-plan.js +220 -0
- package/dist/goal-planner-prompt.js +28 -0
- package/dist/goal-prompt-budget.js +4 -0
- package/dist/goal-reference-images.js +25 -0
- package/dist/goal-state.js +261 -0
- package/dist/goal-stills.js +153 -0
- package/dist/goal-store.js +144 -0
- package/dist/goal-styles.js +144 -0
- package/dist/goal-v3-runtime.js +157 -0
- package/dist/guides/character_schema_guide.md +70 -0
- package/dist/guides/gpt_image_2_5_prompting_guide.md +21 -0
- package/dist/guides/grok_imagine_prompting_guide.md +72 -0
- package/dist/guides/image_reproduction_guide.md +84 -0
- package/dist/guides/kling_3_0_prompting_guide.md +136 -0
- package/dist/guides/nano_banana_pro_prompting_guide.md +28 -0
- package/dist/guides/seedance_2_prompting_guide.md +172 -0
- package/dist/http-access.js +49 -0
- package/dist/http-byte-range.js +31 -0
- package/dist/image-compress.js +77 -0
- package/dist/image-edit-default.js +1 -0
- package/dist/image-edit-models.js +41 -0
- package/dist/image-edit-settings.js +11 -0
- package/dist/image-region-edit.js +32 -0
- package/dist/image-subject-map.js +523 -0
- package/dist/image.js +46 -0
- package/dist/index.js +12 -0
- package/dist/job-deadline.js +60 -0
- package/dist/jobs/_alibaba-upload.js +30 -0
- package/dist/jobs/_audio-chunks.js +31 -0
- package/dist/jobs/_download-into-asset.js +15 -0
- package/dist/jobs/_fal-shared.js +22 -0
- package/dist/jobs/_fal-upload.js +35 -0
- package/dist/jobs/_image-edit-adapter.js +213 -0
- package/dist/jobs/_kie-poll.js +119 -0
- package/dist/jobs/_kie-upload.js +32 -0
- package/dist/jobs/_lease.js +163 -0
- package/dist/jobs/_persist-output.js +19 -0
- package/dist/jobs/_storage-retry.js +21 -0
- package/dist/jobs/_transcribe.js +245 -0
- package/dist/jobs/_translate-transcript.js +270 -0
- package/dist/jobs/analyze-alive-captions.js +10 -0
- package/dist/jobs/analyze-editing-moments.js +104 -0
- package/dist/jobs/analyze-jokes.js +143 -0
- package/dist/jobs/analyze-keyword-placement.js +155 -0
- package/dist/jobs/analyze-music.js +14 -0
- package/dist/jobs/analyze-video.js +282 -0
- package/dist/jobs/apply-cuts.js +83 -0
- package/dist/jobs/apply-sfx.js +107 -0
- package/dist/jobs/archive-clip.js +457 -0
- package/dist/jobs/assemble-goal.js +98 -0
- package/dist/jobs/audio-boost.js +71 -0
- package/dist/jobs/audio-mix.js +161 -0
- package/dist/jobs/backfill-asset-tags.js +144 -0
- package/dist/jobs/burn-watermark.js +316 -0
- package/dist/jobs/change-video-speed.js +176 -0
- package/dist/jobs/clean-video-audio.js +207 -0
- package/dist/jobs/cold-open.js +166 -0
- package/dist/jobs/combine-goal.js +108 -0
- package/dist/jobs/complete-provider-task.js +185 -0
- package/dist/jobs/copy-library-asset.js +33 -0
- package/dist/jobs/cost-estimate-assessment.js +113 -0
- package/dist/jobs/crop-image.js +47 -0
- package/dist/jobs/crop-video.js +71 -0
- package/dist/jobs/describe-image.js +181 -0
- package/dist/jobs/detect-scenes.js +33 -0
- package/dist/jobs/doctor-fix.js +100 -0
- package/dist/jobs/download-video.js +426 -0
- package/dist/jobs/enqueue-search-index.js +24 -0
- package/dist/jobs/even-split.js +122 -0
- package/dist/jobs/export-sequence.js +365 -0
- package/dist/jobs/extend-audio.js +229 -0
- package/dist/jobs/extract-audio.js +72 -0
- package/dist/jobs/extract-frame.js +58 -0
- package/dist/jobs/extract-notebook-audio.js +82 -0
- package/dist/jobs/extract-notebook-frame.js +93 -0
- package/dist/jobs/extract-video-clip.js +8 -0
- package/dist/jobs/extract-video-poster.js +55 -0
- package/dist/jobs/extract-waveform.js +25 -0
- package/dist/jobs/finish-goal.js +255 -0
- package/dist/jobs/forget-asset.js +11 -0
- package/dist/jobs/generate-acestep-cover.js +219 -0
- package/dist/jobs/generate-media.js +70 -0
- package/dist/jobs/image-subjects.js +1059 -0
- package/dist/jobs/index.js +18 -0
- package/dist/jobs/inject-first-frame.js +175 -0
- package/dist/jobs/isolate-vocals.js +69 -0
- package/dist/jobs/kie-paygate.js +177 -0
- package/dist/jobs/label-music-sections.js +5 -0
- package/dist/jobs/osg-placement.js +135 -0
- package/dist/jobs/pad-video.js +72 -0
- package/dist/jobs/plan-goal.js +97 -0
- package/dist/jobs/post-record.js +21 -0
- package/dist/jobs/post-to-x.js +21 -0
- package/dist/jobs/prepare-search-reference.js +190 -0
- package/dist/jobs/prepare-temporal-search.js +16 -0
- package/dist/jobs/process-audio.js +67 -0
- package/dist/jobs/process-image.js +58 -0
- package/dist/jobs/process-upload.js +166 -0
- package/dist/jobs/process-video.js +438 -0
- package/dist/jobs/register-all.js +161 -0
- package/dist/jobs/relayout-notebook.js +78 -0
- package/dist/jobs/remove-sound.js +299 -0
- package/dist/jobs/render-project.js +380 -0
- package/dist/jobs/render-qr-outro.js +138 -0
- package/dist/jobs/render-timeline.js +283 -0
- package/dist/jobs/repair-search-indexes.js +42 -0
- package/dist/jobs/replace-video-audio.js +64 -0
- package/dist/jobs/restore-archive-clip.js +53 -0
- package/dist/jobs/reverse-video.js +53 -0
- package/dist/jobs/rewrite-script.js +173 -0
- package/dist/jobs/run-notebook.js +3335 -0
- package/dist/jobs/semantic-index.js +797 -0
- package/dist/jobs/smooth-scene-concat.js +257 -0
- package/dist/jobs/splice-video.js +280 -0
- package/dist/jobs/split-video.js +127 -0
- package/dist/jobs/suggest-cuts.js +149 -0
- package/dist/jobs/suggest-sfx.js +135 -0
- package/dist/jobs/sync-video-to-audio.js +329 -0
- package/dist/jobs/tag-asset.js +266 -0
- package/dist/jobs/task-failure.js +16 -0
- package/dist/jobs/timewarp-video.js +152 -0
- package/dist/jobs/transcribe-video-audio.js +117 -0
- package/dist/jobs/transcribe.js +35 -0
- package/dist/jobs/translate-transcript.js +55 -0
- package/dist/jobs/trim-audio.js +133 -0
- package/dist/jobs/trim-video.js +291 -0
- package/dist/jobs/xai-paygate.js +26 -0
- package/dist/keyword-placement-cache.js +74 -0
- package/dist/lazy-render.js +142 -0
- package/dist/library-browse.js +78 -0
- package/dist/lightning-defaults.js +206 -0
- package/dist/load-env.js +9 -0
- package/dist/local-search-models.js +206 -0
- package/dist/local-upload.js +318 -0
- package/dist/logger.js +69 -0
- package/dist/media/adapters/alibaba.js +20 -0
- package/dist/media/adapters/elevenlabs.js +58 -0
- package/dist/media/adapters/fal.js +45 -0
- package/dist/media/adapters/index.js +26 -0
- package/dist/media/adapters/kie.js +120 -0
- package/dist/media/adapters/pending.js +21 -0
- package/dist/media/adapters/types.js +5 -0
- package/dist/media/adapters/xai.js +18 -0
- package/dist/media/catalog.js +236 -0
- package/dist/media/complete.js +191 -0
- package/dist/media/cost.js +45 -0
- package/dist/media/descriptor.js +157 -0
- package/dist/media/endpoints.js +11 -0
- package/dist/media/frame-options.js +18 -0
- package/dist/media/insert-video-timing.js +17 -0
- package/dist/media/json-schema.js +75 -0
- package/dist/media/legacy-task-types.js +129 -0
- package/dist/media/listing.js +24 -0
- package/dist/media/model-id.js +14 -0
- package/dist/media/preview.js +86 -0
- package/dist/media/prompt-dialects.js +65 -0
- package/dist/media/ref-fields.js +106 -0
- package/dist/media/refs.js +131 -0
- package/dist/media/request.js +188 -0
- package/dist/media/types.js +6 -0
- package/dist/media/wire.js +161 -0
- package/dist/media-library.js +535 -0
- package/dist/media-output-path.js +33 -0
- package/dist/multyculty-workflow.js +757 -0
- package/dist/notebook-action-catalog.js +608 -0
- package/dist/notebook-cell-outputs.js +726 -0
- package/dist/notebook-credit-retry.js +45 -0
- package/dist/notebook-fixtures.js +8 -0
- package/dist/notebook-generation-contract.js +77 -0
- package/dist/notebook-generation-controls.js +145 -0
- package/dist/notebook-goal-layout.js +40 -0
- package/dist/notebook-graph.js +37 -0
- package/dist/notebook-image-edit.js +84 -0
- package/dist/notebook-initial-generation-prompt.js +222 -0
- package/dist/notebook-live-analysis.js +941 -0
- package/dist/notebook-media-ops.js +579 -0
- package/dist/notebook-mutations.js +20 -0
- package/dist/notebook-placement.js +41 -0
- package/dist/notebook-provider-failure.js +87 -0
- package/dist/notebook-published-post.js +26 -0
- package/dist/notebook-reference-resolver.js +227 -0
- package/dist/notebook-relayout.js +270 -0
- package/dist/notebook-run-plan.js +519 -0
- package/dist/notebook-shots.js +162 -0
- package/dist/notebook-workflow.js +856 -0
- package/dist/openai-intelligence.js +99 -0
- package/dist/original-media-file.js +12 -0
- package/dist/osg-assets.js +36 -0
- package/dist/osg-placement-cache.js +64 -0
- package/dist/osg-settings.js +27 -0
- package/dist/paid-tools.js +22 -0
- package/dist/platform-guidance.js +26 -0
- package/dist/posts/dropbox-auth.js +91 -0
- package/dist/posts/dropbox-upload.js +132 -0
- package/dist/posts/icloud-drive.js +134 -0
- package/dist/posts/icloud-status.js +64 -0
- package/dist/posts/post-contract.js +2 -0
- package/dist/posts/post-copy-format.js +43 -0
- package/dist/posts/post-copy.js +193 -0
- package/dist/posts/post-folder.js +18 -0
- package/dist/posts/post-metrics.js +119 -0
- package/dist/posts/post-store.js +340 -0
- package/dist/posts/x-post-target.js +43 -0
- package/dist/preview-payload.js +90 -0
- package/dist/prompt-surfaces.js +22 -0
- package/dist/provider-prompt-transform.js +77 -0
- package/dist/provider-status-link.js +21 -0
- package/dist/provider-task-record.js +78 -0
- package/dist/provider-task-status.js +47 -0
- package/dist/provider-usage.js +294 -0
- package/dist/queue.js +806 -0
- package/dist/reference-video-duration.js +17 -0
- package/dist/remotion/BlurFillVideo.js +51 -0
- package/dist/remotion/KeywordGraphicsOverlay.js +68 -0
- package/dist/remotion/OsgOverlay.js +30 -0
- package/dist/remotion/QrOutroComposition.js +47 -0
- package/dist/remotion/RenderComposition.js +11 -0
- package/dist/remotion/browser.js +59 -0
- package/dist/remotion/constants.js +35 -0
- package/dist/remotion/index.js +66 -0
- package/dist/remotion/keyword-graphics.js +151 -0
- package/dist/remotion/keyword-placement.js +182 -0
- package/dist/remotion/osg-spec.js +198 -0
- package/dist/remotion/public/qr-outro-impact.wav +0 -0
- package/dist/remotion/qr-outro-audio.js +1 -0
- package/dist/remotion/qr-outro-spec.js +211 -0
- package/dist/remotion/shared-bundle.js +37 -0
- package/dist/remotion/subtitles/AliveCaptions.js +83 -0
- package/dist/remotion/subtitles/Highlight.js +58 -0
- package/dist/remotion/subtitles/Impact.js +67 -0
- package/dist/remotion/subtitles/SubtitleOverlay.js +9 -0
- package/dist/remotion/subtitles/alive-layout.js +321 -0
- package/dist/remotion/subtitles/alive-types.js +17 -0
- package/dist/remotion/subtitles/chunker.js +68 -0
- package/dist/remotion/subtitles/segments.js +42 -0
- package/dist/remotion/subtitles/styles.js +4 -0
- package/dist/remotion/subtitles/types.js +1 -0
- package/dist/remotion/util.js +18 -0
- package/dist/remotion-cancellation.js +15 -0
- package/dist/rename-helpers.js +204 -0
- package/dist/rhythm/analyze.js +128 -0
- package/dist/rhythm/contract.js +14 -0
- package/dist/rhythm/draft-model.js +19 -0
- package/dist/rhythm/energy.js +133 -0
- package/dist/rhythm/fft.js +44 -0
- package/dist/rhythm/model.js +71 -0
- package/dist/rhythm/music-analysis.js +124 -0
- package/dist/rhythm/music-draft.js +261 -0
- package/dist/rhythm/onset.js +101 -0
- package/dist/rhythm/run-worker.js +51 -0
- package/dist/rhythm/score.js +47 -0
- package/dist/rhythm/section-labels.js +168 -0
- package/dist/rhythm/sequence-rhythm.js +67 -0
- package/dist/rhythm/synthesize.js +103 -0
- package/dist/rhythm/timing.js +98 -0
- package/dist/rhythm/track.js +320 -0
- package/dist/rhythm/visual-fit.js +4 -0
- package/dist/rhythm/worker.js +25 -0
- package/dist/scene-detect.js +200 -0
- package/dist/scene-transition.js +12 -0
- package/dist/scheduled-posts.js +200 -0
- package/dist/search-index-coverage.js +13 -0
- package/dist/search-index-media.js +71 -0
- package/dist/search-reference-store.js +60 -0
- package/dist/search-scoring.js +17 -0
- package/dist/serve.js +150 -0
- package/dist/server-lock.js +143 -0
- package/dist/server.js +42 -0
- package/dist/srt.js +16 -0
- package/dist/tag-backfill-state.js +121 -0
- package/dist/tag-settings.js +30 -0
- package/dist/tag-vocabulary.js +237 -0
- package/dist/test-queue-cleanup.js +1 -0
- package/dist/time-parse.js +50 -0
- package/dist/timeline-helpers.js +554 -0
- package/dist/timewarp.js +87 -0
- package/dist/tools/_image-inputs.js +9 -0
- package/dist/tools/_media-refs.js +84 -0
- package/dist/tools/_shared.js +59 -0
- package/dist/tools/_suggest-shared.js +279 -0
- package/dist/tools/add-timeline-audio.js +73 -0
- package/dist/tools/alive-captions.js +157 -0
- package/dist/tools/analyze-jokes.js +56 -0
- package/dist/tools/analyze-video.js +66 -0
- package/dist/tools/animate-keywords.js +217 -0
- package/dist/tools/api-status.js +42 -0
- package/dist/tools/application-commands.js +34 -0
- package/dist/tools/apply-cuts.js +112 -0
- package/dist/tools/apply-sfx.js +152 -0
- package/dist/tools/apply-watermark.js +670 -0
- package/dist/tools/archive-clip.js +74 -0
- package/dist/tools/asset-history.js +61 -0
- package/dist/tools/asset-manifest.js +24 -0
- package/dist/tools/asset-tags.js +151 -0
- package/dist/tools/assign-slot.js +97 -0
- package/dist/tools/audio-boost.js +107 -0
- package/dist/tools/audio-mix.js +121 -0
- package/dist/tools/book-usage.js +16 -0
- package/dist/tools/chain.js +136 -0
- package/dist/tools/change-video-speed.js +105 -0
- package/dist/tools/chat-history.js +5 -0
- package/dist/tools/chat.js +848 -0
- package/dist/tools/clean-video-audio.js +97 -0
- package/dist/tools/clear-failed-tasks.js +66 -0
- package/dist/tools/cold-open.js +114 -0
- package/dist/tools/command-plan.js +1680 -0
- package/dist/tools/commands.js +1666 -0
- package/dist/tools/concat-images.js +236 -0
- package/dist/tools/connect-service.js +55 -0
- package/dist/tools/copy-file-to-clipboard.js +58 -0
- package/dist/tools/create-character.js +114 -0
- package/dist/tools/create-qart.js +253 -0
- package/dist/tools/create-qr-outro.js +149 -0
- package/dist/tools/create-qrbtf.js +215 -0
- package/dist/tools/create-watermark.js +97 -0
- package/dist/tools/crop-asset.js +18 -0
- package/dist/tools/crop-image.js +120 -0
- package/dist/tools/crop-video.js +97 -0
- package/dist/tools/delete-asset.js +104 -0
- package/dist/tools/describe-image.js +69 -0
- package/dist/tools/detect-scenes.js +44 -0
- package/dist/tools/doctor.js +126 -0
- package/dist/tools/download-video.js +114 -0
- package/dist/tools/duplicate-asset.js +184 -0
- package/dist/tools/edit-timeline.js +59 -0
- package/dist/tools/editing-moments.js +105 -0
- package/dist/tools/estimate-cost.js +171 -0
- package/dist/tools/even-split.js +295 -0
- package/dist/tools/export-asset-file.js +22 -0
- package/dist/tools/export-asset.js +140 -0
- package/dist/tools/export-sequence.js +250 -0
- package/dist/tools/export-to-downloads.js +54 -0
- package/dist/tools/export-transcript-srt.js +79 -0
- package/dist/tools/extend-audio.js +100 -0
- package/dist/tools/extract-audio.js +174 -0
- package/dist/tools/extract-frame.js +108 -0
- package/dist/tools/extract-notebook-audio.js +173 -0
- package/dist/tools/extract-notebook-frame.js +160 -0
- package/dist/tools/extract-video-clip.js +101 -0
- package/dist/tools/generate-media.js +157 -0
- package/dist/tools/generation-payload.js +48 -0
- package/dist/tools/get-timeline.js +12 -0
- package/dist/tools/get-watermark-config.js +38 -0
- package/dist/tools/goal.js +644 -0
- package/dist/tools/image-subjects.js +711 -0
- package/dist/tools/index.js +323 -0
- package/dist/tools/inject-first-frame.js +154 -0
- package/dist/tools/invert-image.js +70 -0
- package/dist/tools/isolate-vocals.js +66 -0
- package/dist/tools/list-assets.js +98 -0
- package/dist/tools/list-cell-generations.js +225 -0
- package/dist/tools/list-elevenlabs-voices.js +163 -0
- package/dist/tools/log-wrapper.js +29 -0
- package/dist/tools/make-cover-song.js +95 -0
- package/dist/tools/mark-asset-seen.js +23 -0
- package/dist/tools/media-library.js +245 -0
- package/dist/tools/move-timeline-audio.js +35 -0
- package/dist/tools/music-analysis.js +60 -0
- package/dist/tools/music-draft.js +29 -0
- package/dist/tools/pad-video.js +87 -0
- package/dist/tools/post-record.js +83 -0
- package/dist/tools/post.js +168 -0
- package/dist/tools/prompt-history.js +164 -0
- package/dist/tools/provider-key-status.js +248 -0
- package/dist/tools/qrbtf-render.js +849 -0
- package/dist/tools/read-asset-metadata.js +26 -0
- package/dist/tools/read-file-at-commit.js +29 -0
- package/dist/tools/read-transcript.js +41 -0
- package/dist/tools/reconcile-project-work.js +1452 -0
- package/dist/tools/recover-asset.js +28 -0
- package/dist/tools/remove-sound.js +72 -0
- package/dist/tools/remove-timeline-audio.js +34 -0
- package/dist/tools/remove-timeline-slot.js +40 -0
- package/dist/tools/rename-all.js +434 -0
- package/dist/tools/rename-asset.js +34 -0
- package/dist/tools/render-settings.js +47 -0
- package/dist/tools/render-timeline.js +120 -0
- package/dist/tools/render.js +384 -0
- package/dist/tools/reorder-character-images.js +45 -0
- package/dist/tools/replace-video-audio.js +133 -0
- package/dist/tools/restore-archive-clips.js +209 -0
- package/dist/tools/restore-asset-version.js +37 -0
- package/dist/tools/retry-generation.js +54 -0
- package/dist/tools/reverse-video.js +74 -0
- package/dist/tools/rewrite-prompt.js +388 -0
- package/dist/tools/rewrite-script.js +67 -0
- package/dist/tools/schema-from-apicity.js +66 -0
- package/dist/tools/search-archive.js +150 -0
- package/dist/tools/semantic-search.js +1214 -0
- package/dist/tools/sequence-rhythm.js +43 -0
- package/dist/tools/set-character-metadata.js +84 -0
- package/dist/tools/set-env-secret.js +40 -0
- package/dist/tools/set-osg.js +143 -0
- package/dist/tools/show-folder.js +37 -0
- package/dist/tools/smooth-scene-concat.js +122 -0
- package/dist/tools/splice-video.js +149 -0
- package/dist/tools/split-video.js +202 -0
- package/dist/tools/suggest-cuts.js +45 -0
- package/dist/tools/suggest-sfx.js +43 -0
- package/dist/tools/sync-video-to-audio.js +115 -0
- package/dist/tools/tag-backfill.js +177 -0
- package/dist/tools/timewarp-video.js +85 -0
- package/dist/tools/tool-def.js +1 -0
- package/dist/tools/tool-json-schema.js +116 -0
- package/dist/tools/transcribe.js +90 -0
- package/dist/tools/translate-subtitles.js +59 -0
- package/dist/tools/trim-audio.js +139 -0
- package/dist/tools/trim-video.js +168 -0
- package/dist/tools/update-script.js +25 -0
- package/dist/tools/update-timeline-audio.js +40 -0
- package/dist/tools/upload-file.js +41 -0
- package/dist/tools/verify-x-mentions.js +141 -0
- package/dist/tools/video-editing-command-contract.js +291 -0
- package/dist/tools/video-source-reference.js +30 -0
- package/dist/tools/video-time-input.js +18 -0
- package/dist/tools/videobook-domain.js +2006 -0
- package/dist/tools/wait-for-asset-ready.js +143 -0
- package/dist/tools/workspace-actions.js +54 -0
- package/dist/transcript-projection.js +202 -0
- package/dist/transcript-srt.js +33 -0
- package/dist/upload.js +76 -0
- package/dist/usage-providers.js +25 -0
- package/dist/video-analysis-projection.js +46 -0
- package/dist/video-poster.js +33 -0
- package/dist/video-proxy.js +99 -0
- package/dist/vision-regions.js +111 -0
- package/dist/voice-actions.js +90 -0
- package/dist/voice-audio.js +55 -0
- package/dist/voice-config.js +122 -0
- package/dist/voice-continuation.js +54 -0
- package/dist/voice-contract.js +1 -0
- package/dist/voice-http.js +145 -0
- package/dist/voice-reply-progress.js +55 -0
- package/dist/voice-session.js +992 -0
- package/dist/watermark-badge.js +259 -0
- package/dist/watermark-config.js +111 -0
- package/dist/watermark-handle-position.js +55 -0
- package/dist/watermark-platform.js +15 -0
- package/dist/watermark-timing.js +16 -0
- package/dist/workspace-action-catalog.js +315 -0
- package/dist/workspace-action-contract.js +21 -0
- package/dist/workspace-action-groups.js +98 -0
- package/dist/workspace-action-surfaces.js +7 -0
- package/dist/x/account.js +88 -0
- package/dist/x/authenticated-handle.js +30 -0
- package/dist/x/oauth-flow.js +191 -0
- package/dist/x/post-contract.js +1 -0
- package/dist/x/post-metadata.js +44 -0
- package/dist/x/post-to-x.js +456 -0
- package/dist/x/published-post-cell.js +90 -0
- package/dist/x/x-tokens.js +69 -0
- package/dist/zod-slots.js +334 -0
- package/package.json +92 -4
- package/ui/assets/alive-captions-BWb5nLKj.css +1 -0
- package/ui/assets/alive-captions-DPEr1nKM.js +1 -0
- package/ui/assets/confirm-bodies-D8ouHXKy.js +1 -0
- package/ui/assets/confirm-bodies-DrMhaXC6.css +1 -0
- package/ui/assets/fullscreen-B7nw4YbE.js +1 -0
- package/ui/assets/goal-ui-DK-xTCK0.js +2 -0
- package/ui/assets/goal-ui-DXdUM2KF.css +1 -0
- package/ui/assets/index-DzPAnCUj.css +1 -0
- package/ui/assets/index-kS1905s5.js +37 -0
- package/ui/assets/inspector-MlzQbTVo.js +1 -0
- package/ui/assets/jobs-ui-CoinOWx0.js +1 -0
- package/ui/assets/music-video-Cbe80kmQ.js +1 -0
- package/ui/assets/music-video-DPaiKKEq.css +1 -0
- package/ui/assets/ui-Oirr9VOf.css +1 -0
- package/ui/assets/ui-xo_Y1yQg.js +2 -0
- package/ui/assets/vendor-D2rfiZ5j.css +1 -0
- package/ui/assets/vendor-DfTCWgKm.js +19 -0
- package/ui/assets/vendor-flow-BXbeWxW7.js +7 -0
- package/ui/assets/vendor-flow-BnuhLJ6X.css +1 -0
- package/ui/assets/vendor-icons-CGqhCIXV.js +1 -0
- package/ui/assets/vendor-mcp-BTlEJcHV.js +67 -0
- package/ui/assets/vendor-react-Dj7D1fD4.js +9 -0
- package/ui/assets/vendor-remotion-CH1HXrlc.js +95 -0
- package/ui/assets/videobook-Qlp9HQFu.js +20 -0
- package/ui/favicon.svg +8 -0
- package/ui/index.html +126 -0
|
@@ -0,0 +1,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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { createX, XError } from "@apicity/x";
|
|
2
|
+
import { v7 as uuidv7 } from "uuid";
|
|
3
|
+
import { getValidAccessToken } from "../x/x-tokens.js";
|
|
4
|
+
import { listBookEngines, queue } from "../queue.js";
|
|
5
|
+
import { getPost, listPosts, patchPostRuntime, readPostRuntime } from "./post-store.js";
|
|
6
|
+
import { POST_METRIC_NAMES } from "./post-contract.js";
|
|
7
|
+
const MILESTONES = [1, 24, 72, 168, 672].map((hours) => hours * 3_600_000);
|
|
8
|
+
export function nextPostMetricAt(postedAt, observedAt) {
|
|
9
|
+
const start = new Date(postedAt).getTime();
|
|
10
|
+
const observed = new Date(observedAt).getTime();
|
|
11
|
+
const next = MILESTONES.map((offset) => start + offset).find((time) => time > observed);
|
|
12
|
+
return next === undefined ? null : new Date(next).toISOString();
|
|
13
|
+
}
|
|
14
|
+
export function recordPostMetrics(engine, id, snapshot) {
|
|
15
|
+
const post = getPost(engine, id);
|
|
16
|
+
if (!post.runtime.postedAt)
|
|
17
|
+
throw new Error("Link the published post before recording results");
|
|
18
|
+
const time = new Date(snapshot.observedAt).getTime();
|
|
19
|
+
if (!Number.isFinite(time) || time > Date.now() + 60_000 || time < new Date(post.runtime.postedAt).getTime())
|
|
20
|
+
throw new Error("Observation time must be after publication and no later than now");
|
|
21
|
+
for (const name of POST_METRIC_NAMES) {
|
|
22
|
+
const value = snapshot.values[name];
|
|
23
|
+
if (value !== undefined && value !== null && (!Number.isSafeInteger(value) || value < 0))
|
|
24
|
+
throw new Error(`Invalid ${name}: use a nonnegative whole number or leave it unknown`);
|
|
25
|
+
}
|
|
26
|
+
const runtime = readPostRuntime(engine, id);
|
|
27
|
+
const metrics = [...runtime.metrics, { ...snapshot, id: uuidv7(), observedAt: new Date(time).toISOString() }].sort((a, b) => a.observedAt.localeCompare(b.observedAt));
|
|
28
|
+
patchPostRuntime(engine, id, { metrics });
|
|
29
|
+
}
|
|
30
|
+
export async function refreshPostMetrics(engine, id, options = {}) {
|
|
31
|
+
const post = getPost(engine, id);
|
|
32
|
+
if (post.definition.platform !== "x" || !post.runtime.url || !post.runtime.postedAt)
|
|
33
|
+
throw new Error("Automatic metrics need a linked X publication");
|
|
34
|
+
const tweetId = post.runtime.url.match(/\/status\/(\d+)/)?.[1];
|
|
35
|
+
if (!tweetId)
|
|
36
|
+
throw new Error("Invalid X post link");
|
|
37
|
+
try {
|
|
38
|
+
const x = options.x ?? createX({ accessToken: await getValidAccessToken() });
|
|
39
|
+
const young = Date.now() - new Date(post.runtime.postedAt).getTime() < 30 * 86_400_000;
|
|
40
|
+
const lookup = (privateMetrics) => x.get.v2.tweets(tweetId, {
|
|
41
|
+
"tweet.fields": ["created_at", "author_id", "public_metrics", ...(privateMetrics ? ["non_public_metrics"] : [])],
|
|
42
|
+
expansions: ["attachments.media_keys"],
|
|
43
|
+
"media.fields": ["media_key", "type", "public_metrics"],
|
|
44
|
+
}, options.signal);
|
|
45
|
+
let result;
|
|
46
|
+
try {
|
|
47
|
+
result = await lookup(young);
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (!young || !(error instanceof XError) || ![400, 403].includes(error.status))
|
|
51
|
+
throw error;
|
|
52
|
+
result = await lookup(false);
|
|
53
|
+
}
|
|
54
|
+
if (!result.data)
|
|
55
|
+
throw new Error("X post is unavailable, private or deleted");
|
|
56
|
+
const publicMetrics = result.data.public_metrics ?? {};
|
|
57
|
+
const privateMetrics = result.data.non_public_metrics ?? {};
|
|
58
|
+
const values = {};
|
|
59
|
+
const mapping = { impressions: "impression_count", likes: "like_count", replies: "reply_count", reposts: "retweet_count", quotes: "quote_count", bookmarks: "bookmark_count" };
|
|
60
|
+
for (const [name, source] of Object.entries(mapping)) {
|
|
61
|
+
const value = publicMetrics[source];
|
|
62
|
+
if (typeof value === "number")
|
|
63
|
+
values[name] = value;
|
|
64
|
+
}
|
|
65
|
+
if (typeof privateMetrics.url_link_clicks === "number")
|
|
66
|
+
values.clicks = privateMetrics.url_link_clicks;
|
|
67
|
+
const media = result.includes?.media?.filter((item) => item.type === "video");
|
|
68
|
+
const views = media?.map((item) => item.public_metrics?.view_count).filter((value) => typeof value === "number");
|
|
69
|
+
const snapshot = {
|
|
70
|
+
source: "x", observedAt: new Date().toISOString(), values,
|
|
71
|
+
...(views?.length ? { mediaViews: views.reduce((sum, count) => sum + count, 0) } : {}),
|
|
72
|
+
note: "X video views belong to the media and may include other posts. Missing metrics are unknown.",
|
|
73
|
+
};
|
|
74
|
+
recordPostMetrics(engine, id, snapshot);
|
|
75
|
+
patchPostRuntime(engine, id, { nextMetricAt: nextPostMetricAt(post.runtime.postedAt, snapshot.observedAt), metricsError: null, metricsPaused: false });
|
|
76
|
+
return snapshot;
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
const paused = (error instanceof XError && [401, 403, 404].includes(error.status))
|
|
80
|
+
|| (error instanceof Error && /access token|token expired|unavailable, private or deleted/i.test(error.message));
|
|
81
|
+
patchPostRuntime(engine, id, {
|
|
82
|
+
metricsError: error instanceof XError ? `X metrics unavailable (${error.status}). Reconnect X or check the post link.` : error instanceof Error ? error.message : "Metrics unavailable",
|
|
83
|
+
metricsPaused: paused,
|
|
84
|
+
nextMetricAt: paused ? null : new Date(Date.now() + (error instanceof XError && error.status === 429 ? 15 : 5) * 60_000).toISOString(),
|
|
85
|
+
});
|
|
86
|
+
throw error;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
let timer = null;
|
|
90
|
+
let polling = false;
|
|
91
|
+
async function collectDuePostMetrics() {
|
|
92
|
+
if (polling)
|
|
93
|
+
return;
|
|
94
|
+
polling = true;
|
|
95
|
+
try {
|
|
96
|
+
for (const engine of await listBookEngines()) {
|
|
97
|
+
for (const post of listPosts(engine)) {
|
|
98
|
+
if (post.definition.platform !== "x" || !post.runtime.url || !post.runtime.nextMetricAt || post.runtime.metricsPaused || new Date(post.runtime.nextMetricAt).getTime() > Date.now())
|
|
99
|
+
continue;
|
|
100
|
+
await queue.enqueue(`post-metrics-${post.id}-${new Date(post.runtime.nextMetricAt).getTime()}`, { type: "post_metrics", bookId: engine.book.get().bookId, postCellId: post.id });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
polling = false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function startPostMetricsScheduler() {
|
|
109
|
+
if (timer)
|
|
110
|
+
return;
|
|
111
|
+
timer = setInterval(() => { void collectDuePostMetrics().catch(() => undefined); }, 60_000);
|
|
112
|
+
timer.unref();
|
|
113
|
+
void collectDuePostMetrics().catch(() => undefined);
|
|
114
|
+
}
|
|
115
|
+
export function stopPostMetricsScheduler() {
|
|
116
|
+
if (timer)
|
|
117
|
+
clearInterval(timer);
|
|
118
|
+
timer = null;
|
|
119
|
+
}
|