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,175 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { isHealthy } from "./client.js";
|
|
3
|
+
import { aceStepConfig, checkLocalAvailability } from "./config.js";
|
|
4
|
+
import { logger } from "../logger.js";
|
|
5
|
+
import { AceStepError } from "./errors.js";
|
|
6
|
+
const state = {
|
|
7
|
+
child: null,
|
|
8
|
+
handle: null,
|
|
9
|
+
starting: null,
|
|
10
|
+
spawnFailure: null,
|
|
11
|
+
lock: Promise.resolve(),
|
|
12
|
+
};
|
|
13
|
+
const HEALTH_POLL_INTERVAL_MS = 500;
|
|
14
|
+
const HEALTH_POLL_TIMEOUT_MS = 30_000;
|
|
15
|
+
const PROBE_TIMEOUT_MS = 700;
|
|
16
|
+
const EXTERNAL_HEALTH_ATTEMPTS = 3;
|
|
17
|
+
const STOP_GRACE_MS = 2_000;
|
|
18
|
+
async function delay(ms) {
|
|
19
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
20
|
+
}
|
|
21
|
+
function drainOutput(stream) {
|
|
22
|
+
if (stream === null)
|
|
23
|
+
return;
|
|
24
|
+
stream.resume();
|
|
25
|
+
}
|
|
26
|
+
// Identity check before adopting a listener on our port: /health must return the
|
|
27
|
+
// documented {"status":"ok"} shape and /props must serve JSON, so we never treat
|
|
28
|
+
// an unrelated local service as ace-server.
|
|
29
|
+
async function probeAceServer(baseUrl) {
|
|
30
|
+
try {
|
|
31
|
+
const health = await fetch(`${baseUrl}/health`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
32
|
+
if (!health.ok)
|
|
33
|
+
return false;
|
|
34
|
+
const healthBody = (await health.json());
|
|
35
|
+
if (healthBody?.status !== "ok")
|
|
36
|
+
return false;
|
|
37
|
+
const props = await fetch(`${baseUrl}/props`, { signal: AbortSignal.timeout(PROBE_TIMEOUT_MS) });
|
|
38
|
+
if (!props.ok)
|
|
39
|
+
return false;
|
|
40
|
+
const propsBody = await props.json();
|
|
41
|
+
return typeof propsBody === "object" && propsBody !== null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function startServer(config, signal) {
|
|
48
|
+
const baseUrl = `http://127.0.0.1:${config.port}`;
|
|
49
|
+
if (await probeAceServer(baseUrl)) {
|
|
50
|
+
const adopted = { baseUrl, owned: false };
|
|
51
|
+
state.handle = adopted;
|
|
52
|
+
return adopted;
|
|
53
|
+
}
|
|
54
|
+
const availability = checkLocalAvailability(config);
|
|
55
|
+
if (!availability.available) {
|
|
56
|
+
throw new AceStepError(availability.kind === "models_missing" ? "models_missing" : "binary_missing", availability.reason);
|
|
57
|
+
}
|
|
58
|
+
const serverArgs = [
|
|
59
|
+
"--models",
|
|
60
|
+
config.modelsDir,
|
|
61
|
+
"--host",
|
|
62
|
+
"127.0.0.1",
|
|
63
|
+
"--port",
|
|
64
|
+
String(config.port),
|
|
65
|
+
];
|
|
66
|
+
if (config.keepLoaded)
|
|
67
|
+
serverArgs.push("--keep-loaded");
|
|
68
|
+
const child = spawn(config.serverPath, serverArgs, { stdio: ["ignore", "pipe", "pipe"] });
|
|
69
|
+
drainOutput(child.stdout);
|
|
70
|
+
drainOutput(child.stderr);
|
|
71
|
+
state.child = child;
|
|
72
|
+
state.spawnFailure = null;
|
|
73
|
+
child.on("error", (error) => {
|
|
74
|
+
state.spawnFailure = new AceStepError("spawn_failed", `Failed to start ace-server at ${config.serverPath}: ${error.message}`);
|
|
75
|
+
});
|
|
76
|
+
child.on("exit", (code, exitSignal) => {
|
|
77
|
+
logger.error("ace-server-exited", { exitCode: code, signal: exitSignal });
|
|
78
|
+
if (state.child === child) {
|
|
79
|
+
state.child = null;
|
|
80
|
+
state.handle = null;
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
const deadline = Date.now() + HEALTH_POLL_TIMEOUT_MS;
|
|
84
|
+
while (Date.now() < deadline) {
|
|
85
|
+
if (signal?.aborted) {
|
|
86
|
+
child.kill("SIGKILL");
|
|
87
|
+
state.child = null;
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
}
|
|
90
|
+
if (state.spawnFailure !== null)
|
|
91
|
+
throw state.spawnFailure;
|
|
92
|
+
if (child.exitCode !== null) {
|
|
93
|
+
throw new AceStepError("spawn_failed", `ace-server exited during startup (code ${child.exitCode}); run the configured server directly to inspect its output`);
|
|
94
|
+
}
|
|
95
|
+
if (await isHealthy(baseUrl, PROBE_TIMEOUT_MS)) {
|
|
96
|
+
const owned = { baseUrl, owned: true };
|
|
97
|
+
state.handle = owned;
|
|
98
|
+
return owned;
|
|
99
|
+
}
|
|
100
|
+
await delay(HEALTH_POLL_INTERVAL_MS);
|
|
101
|
+
}
|
|
102
|
+
child.kill("SIGKILL");
|
|
103
|
+
state.child = null;
|
|
104
|
+
throw new AceStepError("health_timeout", `ace-server did not become healthy on ${baseUrl} within ${HEALTH_POLL_TIMEOUT_MS}ms`);
|
|
105
|
+
}
|
|
106
|
+
export async function ensureAceServer(options = {}) {
|
|
107
|
+
const config = aceStepConfig();
|
|
108
|
+
if (config.baseUrl !== null) {
|
|
109
|
+
for (let attempt = 0; attempt < EXTERNAL_HEALTH_ATTEMPTS; attempt += 1) {
|
|
110
|
+
if (await probeAceServer(config.baseUrl))
|
|
111
|
+
return { baseUrl: config.baseUrl, owned: false };
|
|
112
|
+
if (attempt < EXTERNAL_HEALTH_ATTEMPTS - 1)
|
|
113
|
+
await delay(PROBE_TIMEOUT_MS);
|
|
114
|
+
}
|
|
115
|
+
throw new AceStepError("unreachable", `ace-server at ${config.baseUrl} is not responding (VIDEOBOOK_ACESTEP_BASE_URL). Start it or unset the variable.`);
|
|
116
|
+
}
|
|
117
|
+
if (state.handle !== null && (await isHealthy(state.handle.baseUrl, PROBE_TIMEOUT_MS))) {
|
|
118
|
+
return state.handle;
|
|
119
|
+
}
|
|
120
|
+
if (state.starting !== null)
|
|
121
|
+
return state.starting;
|
|
122
|
+
state.starting = startServer(config, options.signal);
|
|
123
|
+
try {
|
|
124
|
+
return await state.starting;
|
|
125
|
+
}
|
|
126
|
+
finally {
|
|
127
|
+
state.starting = null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ace-server runs one GPU job at a time on a FIFO worker thread and evicts
|
|
131
|
+
// completed jobs past a small cap while holding every result in memory, so we
|
|
132
|
+
// keep at most one videobook synthesis in flight: queued covers wait here
|
|
133
|
+
// instead of piling result buffers onto the server.
|
|
134
|
+
export async function withSynthLock(fn) {
|
|
135
|
+
const previous = state.lock;
|
|
136
|
+
let release;
|
|
137
|
+
state.lock = new Promise((resolve) => {
|
|
138
|
+
release = resolve;
|
|
139
|
+
});
|
|
140
|
+
await previous;
|
|
141
|
+
try {
|
|
142
|
+
return await fn();
|
|
143
|
+
}
|
|
144
|
+
finally {
|
|
145
|
+
release?.();
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
export async function stopAceServer() {
|
|
149
|
+
const child = state.child;
|
|
150
|
+
state.child = null;
|
|
151
|
+
state.handle = null;
|
|
152
|
+
state.starting = null;
|
|
153
|
+
if (child === null || child.exitCode !== null)
|
|
154
|
+
return;
|
|
155
|
+
const exited = new Promise((resolve) => {
|
|
156
|
+
child.once("exit", () => resolve());
|
|
157
|
+
});
|
|
158
|
+
child.kill("SIGTERM");
|
|
159
|
+
const outcome = await Promise.race([
|
|
160
|
+
exited.then(() => "exited"),
|
|
161
|
+
delay(STOP_GRACE_MS).then(() => "timeout"),
|
|
162
|
+
]);
|
|
163
|
+
if (outcome === "timeout") {
|
|
164
|
+
child.kill("SIGKILL");
|
|
165
|
+
await exited;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function resetAceStepForTests() {
|
|
169
|
+
state.child?.kill("SIGKILL");
|
|
170
|
+
state.child = null;
|
|
171
|
+
state.handle = null;
|
|
172
|
+
state.starting = null;
|
|
173
|
+
state.spawnFailure = null;
|
|
174
|
+
state.lock = Promise.resolve();
|
|
175
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { findVideoFile } from "videobook-engine";
|
|
3
|
+
import { getApiKey } from "../api-key.js";
|
|
4
|
+
import { probeVideo } from "../ffmpeg.js";
|
|
5
|
+
import { logger } from "../logger.js";
|
|
6
|
+
import { parseSegments } from "../remotion/subtitles/segments.js";
|
|
7
|
+
import { buildPhraseLines, phraseFrameTime } from "../remotion/subtitles/alive-layout.js";
|
|
8
|
+
import { HOP_SECONDS, VOCAL_SAMPLE_RATE, decodeVocalPcm, detectOnsets, envelopeSeries, rmsFrames, snapWordStarts, wordEnergies, } from "./audio.js";
|
|
9
|
+
import { detectPhraseRegions, extractPhraseFrames } from "./frames.js";
|
|
10
|
+
import { aliveFingerprint, readAliveAnalysis, writeAliveAnalysis } from "./model.js";
|
|
11
|
+
import { ALIVE_ANALYSIS_VERSION } from "./schema.js";
|
|
12
|
+
const ENVELOPE_HZ = 20;
|
|
13
|
+
function segmentInputs(segments, audio, onsets) {
|
|
14
|
+
const words = segments.flatMap((segment) => segment.words);
|
|
15
|
+
const starts = snapWordStarts(words, onsets);
|
|
16
|
+
const energies = audio ? wordEnergies(words, audio.rms, audio.hopSeconds) : words.map(() => 0.5);
|
|
17
|
+
let cursor = 0;
|
|
18
|
+
return segments.map((segment) => segment.words.map((word) => {
|
|
19
|
+
const index = cursor++;
|
|
20
|
+
return { raw: word.word, start: word.start, end: word.end, onsetStart: starts[index], energy: energies[index] };
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
function toPhrase(lines, index) {
|
|
24
|
+
const frameTime = phraseFrameTime(lines);
|
|
25
|
+
return { id: `p${index + 1}`, frameTime: Math.round(frameTime * 1000) / 1000, regions: [], lines };
|
|
26
|
+
}
|
|
27
|
+
/** Phrases, vocal onsets and energy for a transcript; regions are attached separately. */
|
|
28
|
+
function buildAliveAnalysis(input) {
|
|
29
|
+
const { audio } = input;
|
|
30
|
+
const onsets = audio ? detectOnsets(audio.rms, audio.hopSeconds) : [];
|
|
31
|
+
return {
|
|
32
|
+
version: ALIVE_ANALYSIS_VERSION,
|
|
33
|
+
fingerprint: input.fingerprint,
|
|
34
|
+
analyzedAt: new Date().toISOString(),
|
|
35
|
+
durationSeconds: input.durationSeconds,
|
|
36
|
+
sourceWidth: input.sourceWidth,
|
|
37
|
+
sourceHeight: input.sourceHeight,
|
|
38
|
+
envelopeHz: ENVELOPE_HZ,
|
|
39
|
+
envelope: audio ? envelopeSeries(audio.rms, ENVELOPE_HZ, audio.hopSeconds) : [],
|
|
40
|
+
onsets,
|
|
41
|
+
regionsAnalyzed: false,
|
|
42
|
+
phrases: buildPhraseLines(segmentInputs(input.segments, audio, onsets)).map(toPhrase),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function attachRegions(analysis, regions) {
|
|
46
|
+
return {
|
|
47
|
+
...analysis,
|
|
48
|
+
regionsAnalyzed: true,
|
|
49
|
+
phrases: analysis.phrases.map((phrase) => ({ ...phrase, regions: regions.get(phrase.id) ?? [] })),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
/** Locate the video file and timed transcript an analysis is keyed on. */
|
|
53
|
+
export async function aliveSource(engine, assetId) {
|
|
54
|
+
const manifest = await engine.files.manifest(assetId);
|
|
55
|
+
if (!manifest.ok)
|
|
56
|
+
throw new Error(manifest.error.message);
|
|
57
|
+
const videoFile = findVideoFile(manifest.value.files);
|
|
58
|
+
if (!videoFile)
|
|
59
|
+
throw new Error(`No video file found in ${assetId}.`);
|
|
60
|
+
const transcript = await engine.files.read(assetId, "elevenlabs.json");
|
|
61
|
+
if (!transcript.ok)
|
|
62
|
+
throw new Error(`No timed transcript found for ${assetId}. Run /transcribe @${assetId} first.`);
|
|
63
|
+
return {
|
|
64
|
+
videoPath: join(manifest.value.path, videoFile.name),
|
|
65
|
+
fingerprint: aliveFingerprint(videoFile, transcript.value),
|
|
66
|
+
transcript: transcript.value,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
export async function cachedAliveAnalysis(engine, assetId, fingerprint) {
|
|
70
|
+
const cached = await readAliveAnalysis(engine, assetId);
|
|
71
|
+
return cached && cached.fingerprint === fingerprint ? cached : null;
|
|
72
|
+
}
|
|
73
|
+
async function readAudio(videoPath, hasAudio, signal) {
|
|
74
|
+
if (!hasAudio)
|
|
75
|
+
return null;
|
|
76
|
+
const pcm = await decodeVocalPcm(videoPath, signal);
|
|
77
|
+
return pcm ? { rms: rmsFrames(pcm, VOCAL_SAMPLE_RATE, HOP_SECONDS), hopSeconds: HOP_SECONDS } : null;
|
|
78
|
+
}
|
|
79
|
+
async function withRegions(analysis, videoPath, signal) {
|
|
80
|
+
const apiKey = getApiKey("OPENAI_API_KEY");
|
|
81
|
+
if (!apiKey || analysis.phrases.length === 0)
|
|
82
|
+
return analysis;
|
|
83
|
+
try {
|
|
84
|
+
const frames = await extractPhraseFrames(videoPath, analysis.phrases, analysis.durationSeconds, signal);
|
|
85
|
+
return attachRegions(analysis, await detectPhraseRegions(frames, apiKey, signal));
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
signal.throwIfAborted();
|
|
89
|
+
// Placement degrades to the default columns; captions must not fail on vision.
|
|
90
|
+
logger.warn("alive-captions-regions-failed", { error });
|
|
91
|
+
return analysis;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Full analysis for one video: transcript phrases, vocal onsets and energy,
|
|
96
|
+
* then per-phrase face/text regions when a vision provider is configured.
|
|
97
|
+
* Persists and returns the result; a fresh cache hit is returned as-is.
|
|
98
|
+
*/
|
|
99
|
+
export async function analyzeAliveCaptions(engine, assetId, signal, force = false) {
|
|
100
|
+
const source = await aliveSource(engine, assetId);
|
|
101
|
+
const cached = force ? null : await cachedAliveAnalysis(engine, assetId, source.fingerprint);
|
|
102
|
+
if (cached)
|
|
103
|
+
return cached;
|
|
104
|
+
const probe = await probeVideo(source.videoPath, { signal });
|
|
105
|
+
const segments = parseSegments(JSON.parse(source.transcript.toString()));
|
|
106
|
+
const base = buildAliveAnalysis({
|
|
107
|
+
segments,
|
|
108
|
+
audio: await readAudio(source.videoPath, probe.hasAudio, signal),
|
|
109
|
+
durationSeconds: probe.duration,
|
|
110
|
+
sourceWidth: probe.width,
|
|
111
|
+
sourceHeight: probe.height,
|
|
112
|
+
fingerprint: source.fingerprint,
|
|
113
|
+
});
|
|
114
|
+
const analysis = await withRegions(base, source.videoPath, signal);
|
|
115
|
+
await writeAliveAnalysis(engine, assetId, analysis);
|
|
116
|
+
return analysis;
|
|
117
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runFfmpeg } from "../ffmpeg.js";
|
|
5
|
+
/**
|
|
6
|
+
* Vocal timing for alive captions: decode a band-limited mono track, then
|
|
7
|
+
* derive a loudness envelope, vocal onsets and per-word energy from it.
|
|
8
|
+
*/
|
|
9
|
+
export const VOCAL_SAMPLE_RATE = 8_000;
|
|
10
|
+
export const HOP_SECONDS = 0.01;
|
|
11
|
+
const ONSET_HISTORY_FRAMES = 4;
|
|
12
|
+
const ONSET_PEAK_RADIUS = 3;
|
|
13
|
+
const ONSET_MIN_SPACING_SECONDS = 0.06;
|
|
14
|
+
const ONSET_MIN_NOVELTY = 0.35;
|
|
15
|
+
const EPSILON = 1e-4;
|
|
16
|
+
const ENERGY_WINDOW_SECONDS = 0.3;
|
|
17
|
+
const DEFAULT_SNAP_SECONDS = 0.15;
|
|
18
|
+
/** Decode the first audio stream as 8 kHz mono float PCM filtered to the vocal band; callers check the probe for audio first. */
|
|
19
|
+
export async function decodeVocalPcm(inputPath, signal) {
|
|
20
|
+
const dir = await mkdtemp(join(tmpdir(), "videobook-alive-audio-"));
|
|
21
|
+
const output = join(dir, "vocal.f32");
|
|
22
|
+
try {
|
|
23
|
+
await runFfmpeg([
|
|
24
|
+
"-y", "-i", inputPath,
|
|
25
|
+
"-map", "0:a:0", "-vn",
|
|
26
|
+
"-af", "highpass=f=250,lowpass=f=3500",
|
|
27
|
+
"-ac", "1", "-ar", String(VOCAL_SAMPLE_RATE),
|
|
28
|
+
"-f", "f32le", output,
|
|
29
|
+
], { signal });
|
|
30
|
+
const bytes = await readFile(output).catch(() => null);
|
|
31
|
+
if (!bytes || bytes.length < 4)
|
|
32
|
+
return null;
|
|
33
|
+
const aligned = new Uint8Array(bytes.length - (bytes.length % 4));
|
|
34
|
+
aligned.set(bytes.subarray(0, aligned.length));
|
|
35
|
+
return new Float32Array(aligned.buffer);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
await rm(dir, { recursive: true, force: true });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** RMS per hop over a two-hop window. */
|
|
42
|
+
export function rmsFrames(samples, sampleRate, hopSeconds = HOP_SECONDS) {
|
|
43
|
+
const hop = Math.max(1, Math.round(sampleRate * hopSeconds));
|
|
44
|
+
const window = hop * 2;
|
|
45
|
+
const count = Math.ceil(samples.length / hop);
|
|
46
|
+
const frames = new Float32Array(count);
|
|
47
|
+
for (let frame = 0; frame < count; frame++) {
|
|
48
|
+
const begin = frame * hop;
|
|
49
|
+
const end = Math.min(samples.length, begin + window);
|
|
50
|
+
let sum = 0;
|
|
51
|
+
for (let index = begin; index < end; index++)
|
|
52
|
+
sum += samples[index] * samples[index];
|
|
53
|
+
frames[frame] = Math.sqrt(sum / Math.max(1, end - begin));
|
|
54
|
+
}
|
|
55
|
+
return frames;
|
|
56
|
+
}
|
|
57
|
+
function percentile(values, fraction) {
|
|
58
|
+
if (values.length === 0)
|
|
59
|
+
return 0;
|
|
60
|
+
const sorted = Array.from(values).sort((a, b) => a - b);
|
|
61
|
+
const index = Math.min(sorted.length - 1, Math.max(0, Math.floor(fraction * (sorted.length - 1))));
|
|
62
|
+
return sorted[index];
|
|
63
|
+
}
|
|
64
|
+
function historyMean(rms, frame) {
|
|
65
|
+
const from = Math.max(0, frame - ONSET_HISTORY_FRAMES);
|
|
66
|
+
if (frame === from)
|
|
67
|
+
return 0;
|
|
68
|
+
let sum = 0;
|
|
69
|
+
for (let index = from; index < frame; index++)
|
|
70
|
+
sum += rms[index];
|
|
71
|
+
return sum / (frame - from);
|
|
72
|
+
}
|
|
73
|
+
/** Log-energy rise of each frame over its recent history, gated by a noise floor. */
|
|
74
|
+
function onsetNovelty(rms) {
|
|
75
|
+
const floor = percentile(rms, 0.2) * 2 + EPSILON;
|
|
76
|
+
const novelty = new Float32Array(rms.length);
|
|
77
|
+
for (let frame = 0; frame < rms.length; frame++) {
|
|
78
|
+
if (rms[frame] < floor)
|
|
79
|
+
continue;
|
|
80
|
+
novelty[frame] = Math.max(0, Math.log(rms[frame] + EPSILON) - Math.log(historyMean(rms, frame) + EPSILON));
|
|
81
|
+
}
|
|
82
|
+
return novelty;
|
|
83
|
+
}
|
|
84
|
+
function noveltyThreshold(novelty) {
|
|
85
|
+
const active = Array.from(novelty).filter((value) => value > 0);
|
|
86
|
+
if (active.length === 0)
|
|
87
|
+
return Infinity;
|
|
88
|
+
const mean = active.reduce((sum, value) => sum + value, 0) / active.length;
|
|
89
|
+
const variance = active.reduce((sum, value) => sum + (value - mean) ** 2, 0) / active.length;
|
|
90
|
+
return Math.max(ONSET_MIN_NOVELTY, mean + Math.sqrt(variance));
|
|
91
|
+
}
|
|
92
|
+
function isLocalPeak(novelty, frame) {
|
|
93
|
+
const from = Math.max(0, frame - ONSET_PEAK_RADIUS);
|
|
94
|
+
const to = Math.min(novelty.length - 1, frame + ONSET_PEAK_RADIUS);
|
|
95
|
+
for (let index = from; index <= to; index++) {
|
|
96
|
+
if (novelty[index] > novelty[frame])
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
return true;
|
|
100
|
+
}
|
|
101
|
+
/** Onset times in seconds, ascending. */
|
|
102
|
+
export function detectOnsets(rms, hopSeconds = HOP_SECONDS) {
|
|
103
|
+
const novelty = onsetNovelty(rms);
|
|
104
|
+
const threshold = noveltyThreshold(novelty);
|
|
105
|
+
const onsets = [];
|
|
106
|
+
for (let frame = 0; frame < novelty.length; frame++) {
|
|
107
|
+
if (novelty[frame] < threshold || !isLocalPeak(novelty, frame))
|
|
108
|
+
continue;
|
|
109
|
+
const time = frame * hopSeconds;
|
|
110
|
+
if (onsets.length > 0 && time - onsets[onsets.length - 1] < ONSET_MIN_SPACING_SECONDS)
|
|
111
|
+
continue;
|
|
112
|
+
onsets.push(Math.round(time * 1000) / 1000);
|
|
113
|
+
}
|
|
114
|
+
return onsets;
|
|
115
|
+
}
|
|
116
|
+
function firstAtOrAfter(sorted, value) {
|
|
117
|
+
let low = 0;
|
|
118
|
+
let high = sorted.length;
|
|
119
|
+
while (low < high) {
|
|
120
|
+
const mid = (low + high) >> 1;
|
|
121
|
+
if (sorted[mid] < value)
|
|
122
|
+
low = mid + 1;
|
|
123
|
+
else
|
|
124
|
+
high = mid;
|
|
125
|
+
}
|
|
126
|
+
return low;
|
|
127
|
+
}
|
|
128
|
+
/** Nearest onset to `start` within ±maxShift that is not before `notBefore`; else `start`. */
|
|
129
|
+
export function snapToOnset(start, onsets, notBefore, maxShift = DEFAULT_SNAP_SECONDS) {
|
|
130
|
+
const from = firstAtOrAfter(onsets, Math.max(start - maxShift, notBefore));
|
|
131
|
+
let best = start;
|
|
132
|
+
let bestDistance = Infinity;
|
|
133
|
+
for (let index = from; index < onsets.length && onsets[index] <= start + maxShift; index++) {
|
|
134
|
+
const distance = Math.abs(onsets[index] - start);
|
|
135
|
+
if (distance < bestDistance) {
|
|
136
|
+
best = onsets[index];
|
|
137
|
+
bestDistance = distance;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return best;
|
|
141
|
+
}
|
|
142
|
+
/** Snap each word start to a vocal onset, keeping word order strictly increasing. */
|
|
143
|
+
export function snapWordStarts(words, onsets, maxShift = DEFAULT_SNAP_SECONDS) {
|
|
144
|
+
const snapped = [];
|
|
145
|
+
for (const word of words) {
|
|
146
|
+
const previous = snapped.length > 0 ? snapped[snapped.length - 1] + 0.04 : 0;
|
|
147
|
+
snapped.push(snapToOnset(word.start, onsets, previous, maxShift));
|
|
148
|
+
}
|
|
149
|
+
return snapped;
|
|
150
|
+
}
|
|
151
|
+
function meanRms(rms, start, end, hopSeconds) {
|
|
152
|
+
const from = Math.max(0, Math.floor(start / hopSeconds));
|
|
153
|
+
const to = Math.min(rms.length, Math.max(from + 1, Math.ceil(end / hopSeconds)));
|
|
154
|
+
let sum = 0;
|
|
155
|
+
for (let index = from; index < to; index++)
|
|
156
|
+
sum += rms[index];
|
|
157
|
+
return to > from ? sum / (to - from) : 0;
|
|
158
|
+
}
|
|
159
|
+
/** Per-word vocal energy normalized so the loudest ~5% of words reach 1. */
|
|
160
|
+
export function wordEnergies(words, rms, hopSeconds = HOP_SECONDS) {
|
|
161
|
+
const raw = words.map((word) => meanRms(rms, word.start, Math.min(word.end, word.start + ENERGY_WINDOW_SECONDS), hopSeconds));
|
|
162
|
+
const reference = percentile(raw, 0.95);
|
|
163
|
+
if (reference <= 0)
|
|
164
|
+
return words.map(() => 0.5);
|
|
165
|
+
return raw.map((value) => Math.round(Math.min(1, value / reference) * 100) / 100);
|
|
166
|
+
}
|
|
167
|
+
/** Downsample RMS frames to `hz` peak buckets normalized to 0..1. */
|
|
168
|
+
export function envelopeSeries(rms, hz, hopSeconds = HOP_SECONDS) {
|
|
169
|
+
const perBucket = Math.max(1, Math.round(1 / (hz * hopSeconds)));
|
|
170
|
+
const peaks = [];
|
|
171
|
+
for (let begin = 0; begin < rms.length; begin += perBucket) {
|
|
172
|
+
let peak = 0;
|
|
173
|
+
for (let index = begin; index < Math.min(rms.length, begin + perBucket); index++)
|
|
174
|
+
peak = Math.max(peak, rms[index]);
|
|
175
|
+
peaks.push(peak);
|
|
176
|
+
}
|
|
177
|
+
const reference = percentile(peaks, 0.99) || 1;
|
|
178
|
+
return peaks.map((peak) => Math.round(Math.min(1, peak / reference) * 1000) / 1000);
|
|
179
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runFfmpeg } from "../ffmpeg.js";
|
|
5
|
+
import { detectOccupiedRegions } from "../vision-regions.js";
|
|
6
|
+
const FRAMES_PER_REQUEST = 10;
|
|
7
|
+
const SYSTEM_PROMPT = `You are a precise video-layout analyst. Each attached image is a frame from the middle of a sung or spoken phrase that will get large stacked caption typography.
|
|
8
|
+
|
|
9
|
+
For every frame, identify all occupied regions the captions must not cover:
|
|
10
|
+
- every visible human face, including partial faces and faces on posters or screens
|
|
11
|
+
- every existing visible text block, subtitle, caption, title, logo, watermark, or graphic
|
|
12
|
+
|
|
13
|
+
Return one entry for every cue_id. Coordinates use a normalized image scale from 0 to 1000: x and y are the top-left corner, followed by width and height. Use tight boxes around the visible occupied content. Do not return boxes for bodies, clothing, scenery, instruments, microphones, or ordinary objects. Return an empty occupied_regions array only when none of the listed content is visible. Never omit, rename, or duplicate a cue_id.`;
|
|
14
|
+
function clampFrameTime(time, duration) {
|
|
15
|
+
return Math.max(0, Math.min(Math.max(0, duration - 0.05), time));
|
|
16
|
+
}
|
|
17
|
+
export async function extractPhraseFrames(videoPath, phrases, duration, signal) {
|
|
18
|
+
const dir = await mkdtemp(join(tmpdir(), "videobook-alive-frames-"));
|
|
19
|
+
try {
|
|
20
|
+
const frames = [];
|
|
21
|
+
for (const phrase of phrases) {
|
|
22
|
+
signal?.throwIfAborted();
|
|
23
|
+
const frameTime = clampFrameTime(phrase.frameTime, duration);
|
|
24
|
+
const framePath = join(dir, `${phrase.id}.jpg`);
|
|
25
|
+
await runFfmpeg([
|
|
26
|
+
"-y", "-ss", frameTime.toFixed(3), "-i", videoPath,
|
|
27
|
+
"-frames:v", "1", "-vf", "scale=768:768:force_original_aspect_ratio=decrease", "-q:v", "3",
|
|
28
|
+
framePath,
|
|
29
|
+
], { signal });
|
|
30
|
+
frames.push({ id: phrase.id, frameTime, buffer: await readFile(framePath) });
|
|
31
|
+
}
|
|
32
|
+
return frames;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
await rm(dir, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
function batches(items, size) {
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let index = 0; index < items.length; index += size)
|
|
41
|
+
out.push(items.slice(index, index + size));
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Face/text/graphic boxes per phrase frame, normalized to the source frame. */
|
|
45
|
+
export async function detectPhraseRegions(frames, apiKey, signal) {
|
|
46
|
+
const regions = new Map();
|
|
47
|
+
for (const batch of batches(frames, FRAMES_PER_REQUEST)) {
|
|
48
|
+
const found = await detectOccupiedRegions(batch.map((frame) => ({ id: frame.id, label: `timestamp=${frame.frameTime.toFixed(3)}s`, buffer: frame.buffer })), {
|
|
49
|
+
apiKey,
|
|
50
|
+
signal,
|
|
51
|
+
instructions: SYSTEM_PROMPT,
|
|
52
|
+
schemaName: "alive_caption_regions",
|
|
53
|
+
errorLabel: "Alive caption placement",
|
|
54
|
+
});
|
|
55
|
+
for (const [id, boxes] of found)
|
|
56
|
+
regions.set(id, boxes);
|
|
57
|
+
}
|
|
58
|
+
return regions;
|
|
59
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { DEFAULT_ALIVE_SETTINGS } from "../remotion/subtitles/alive-types.js";
|
|
3
|
+
import { ALIVE_ANALYSIS_VERSION, aliveAnalysisSchema, aliveSettingsSchema } from "./schema.js";
|
|
4
|
+
const ALIVE_ANALYSIS_METADATA_KEY = "alive-captions.analysis";
|
|
5
|
+
const ALIVE_SETTINGS_METADATA_KEY = "alive-captions.settings";
|
|
6
|
+
export function aliveFingerprint(videoFile, transcript) {
|
|
7
|
+
return createHash("sha256").update(JSON.stringify({
|
|
8
|
+
version: ALIVE_ANALYSIS_VERSION,
|
|
9
|
+
video: { name: videoFile.name, size: videoFile.sizeBytes, objectHash: videoFile.objectHash },
|
|
10
|
+
transcript: createHash("sha256").update(transcript).digest("hex"),
|
|
11
|
+
})).digest("hex");
|
|
12
|
+
}
|
|
13
|
+
export async function readAliveAnalysis(engine, assetId) {
|
|
14
|
+
const result = await engine.metadata.artifacts.read(assetId, ALIVE_ANALYSIS_METADATA_KEY);
|
|
15
|
+
if (!result.ok)
|
|
16
|
+
return null;
|
|
17
|
+
const parsed = aliveAnalysisSchema.safeParse(result.value);
|
|
18
|
+
return parsed.success ? parsed.data : null;
|
|
19
|
+
}
|
|
20
|
+
export async function writeAliveAnalysis(engine, assetId, analysis) {
|
|
21
|
+
const result = await engine.metadata.artifacts.write(assetId, ALIVE_ANALYSIS_METADATA_KEY, aliveAnalysisSchema.parse(analysis));
|
|
22
|
+
if (!result.ok)
|
|
23
|
+
throw new Error(`Failed to persist alive caption analysis: ${result.error.message}`);
|
|
24
|
+
}
|
|
25
|
+
/** Stored settings merged over the defaults; unreadable fields fall back individually. */
|
|
26
|
+
export async function readAliveSettings(engine, assetId) {
|
|
27
|
+
const result = await engine.metadata.artifacts.read(assetId, ALIVE_SETTINGS_METADATA_KEY);
|
|
28
|
+
if (!result.ok)
|
|
29
|
+
return DEFAULT_ALIVE_SETTINGS;
|
|
30
|
+
const partial = aliveSettingsSchema.partial().safeParse(result.value);
|
|
31
|
+
return partial.success ? { ...DEFAULT_ALIVE_SETTINGS, ...partial.data } : DEFAULT_ALIVE_SETTINGS;
|
|
32
|
+
}
|
|
33
|
+
export async function writeAliveSettings(engine, assetId, settings) {
|
|
34
|
+
const result = await engine.metadata.artifacts.write(assetId, ALIVE_SETTINGS_METADATA_KEY, aliveSettingsSchema.parse(settings));
|
|
35
|
+
if (!result.ok)
|
|
36
|
+
throw new Error(`Failed to persist alive caption settings: ${result.error.message}`);
|
|
37
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ALIVE_MOTIONS, ALIVE_STYLES } from "../remotion/subtitles/alive-types.js";
|
|
3
|
+
/** Client-safe (zod only) schemas for stored alive caption analysis and settings. */
|
|
4
|
+
export const ALIVE_ANALYSIS_VERSION = 1;
|
|
5
|
+
const hexColor = z.string().regex(/^#[0-9a-fA-F]{6}$/);
|
|
6
|
+
export const aliveSettingsSchema = z.object({
|
|
7
|
+
style: z.enum(ALIVE_STYLES),
|
|
8
|
+
motion: z.enum(ALIVE_MOTIONS),
|
|
9
|
+
energySizing: z.boolean(),
|
|
10
|
+
snapToOnsets: z.boolean(),
|
|
11
|
+
avoidFaces: z.boolean(),
|
|
12
|
+
avoidText: z.boolean(),
|
|
13
|
+
textColor: hexColor,
|
|
14
|
+
hitColor: hexColor,
|
|
15
|
+
}).strict();
|
|
16
|
+
const regionSchema = z.object({
|
|
17
|
+
kind: z.enum(["face", "text", "graphic"]),
|
|
18
|
+
confidence: z.number().min(0).max(1),
|
|
19
|
+
x: z.number().min(0).max(1),
|
|
20
|
+
y: z.number().min(0).max(1),
|
|
21
|
+
width: z.number().positive().max(1),
|
|
22
|
+
height: z.number().positive().max(1),
|
|
23
|
+
}).strict();
|
|
24
|
+
const wordSchema = z.object({
|
|
25
|
+
text: z.string().min(1),
|
|
26
|
+
transcriptStart: z.number().nonnegative(),
|
|
27
|
+
transcriptEnd: z.number().nonnegative(),
|
|
28
|
+
onsetStart: z.number().nonnegative(),
|
|
29
|
+
energy: z.number().min(0).max(1),
|
|
30
|
+
}).strict();
|
|
31
|
+
const phraseSchema = z.object({
|
|
32
|
+
id: z.string().min(1),
|
|
33
|
+
frameTime: z.number().nonnegative(),
|
|
34
|
+
regions: z.array(regionSchema),
|
|
35
|
+
lines: z.array(z.array(wordSchema).min(1)).min(1),
|
|
36
|
+
}).strict();
|
|
37
|
+
export const aliveAnalysisSchema = z.object({
|
|
38
|
+
version: z.literal(ALIVE_ANALYSIS_VERSION),
|
|
39
|
+
fingerprint: z.string().min(1),
|
|
40
|
+
analyzedAt: z.string().min(1),
|
|
41
|
+
durationSeconds: z.number().nonnegative(),
|
|
42
|
+
sourceWidth: z.number().positive(),
|
|
43
|
+
sourceHeight: z.number().positive(),
|
|
44
|
+
envelopeHz: z.number().positive(),
|
|
45
|
+
envelope: z.array(z.number().min(0).max(1)),
|
|
46
|
+
onsets: z.array(z.number().nonnegative()),
|
|
47
|
+
regionsAnalyzed: z.boolean(),
|
|
48
|
+
phrases: z.array(phraseSchema),
|
|
49
|
+
}).strict();
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { resolveAliveCues } from "../remotion/subtitles/alive-layout.js";
|
|
2
|
+
import { analyzeAliveCaptions } from "./analyze.js";
|
|
3
|
+
import { readAliveSettings } from "./model.js";
|
|
4
|
+
/** Cues positioned for one render target, analysing the video first when the cache is stale. */
|
|
5
|
+
export async function aliveRenderTrack(engine, assetId, geometry, signal) {
|
|
6
|
+
const analysis = await analyzeAliveCaptions(engine, assetId, signal);
|
|
7
|
+
const settings = await readAliveSettings(engine, assetId);
|
|
8
|
+
return { settings, cues: resolveAliveCues(analysis.phrases, settings, geometry) };
|
|
9
|
+
}
|
package/dist/api-key.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Defensive readers for provider API keys.
|
|
3
|
+
*
|
|
4
|
+
* Tool tests set `process.env.KIE_API_KEY = "test-key"` and enqueue a job
|
|
5
|
+
* without waiting for it. `afterEach` restores the real key, then the
|
|
6
|
+
* background worker dequeues and bills real money. These helpers reject
|
|
7
|
+
* placeholder values at the API call site so the worker fails closed.
|
|
8
|
+
*
|
|
9
|
+
* Job handlers MUST go through these — never read `process.env.*_API_KEY`
|
|
10
|
+
* directly.
|
|
11
|
+
*/
|
|
12
|
+
const PLACEHOLDER_PATTERNS = [/^test-/i, /^sk-test-/i];
|
|
13
|
+
export function isPlaceholderApiKey(value) {
|
|
14
|
+
return PLACEHOLDER_PATTERNS.some((re) => re.test(value));
|
|
15
|
+
}
|
|
16
|
+
export function requireApiKey(name) {
|
|
17
|
+
const value = process.env[name];
|
|
18
|
+
if (!value)
|
|
19
|
+
throw new Error(`${name} environment variable is not set`);
|
|
20
|
+
if (isPlaceholderApiKey(value)) {
|
|
21
|
+
throw new Error(`${name} is a placeholder ("${value}"). Refusing to call the real API. ` +
|
|
22
|
+
`If a test enqueued this job, the worker dequeued it after the test ended — fix the test, not this guard.`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
26
|
+
export function getApiKey(name) {
|
|
27
|
+
const value = process.env[name];
|
|
28
|
+
if (!value)
|
|
29
|
+
return null;
|
|
30
|
+
if (isPlaceholderApiKey(value))
|
|
31
|
+
return null;
|
|
32
|
+
return value;
|
|
33
|
+
}
|