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
package/dist/ffmpeg.js
ADDED
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import * as fsp from "node:fs/promises";
|
|
4
|
+
import { logger } from "./logger.js";
|
|
5
|
+
import { FFMPEG } from "./ffmpeg-binary.js";
|
|
6
|
+
import { FFPROBE } from "./ffprobe-binary.js";
|
|
7
|
+
export { FFMPEG, FFPROBE };
|
|
8
|
+
const execFileAsync = promisify(execFile);
|
|
9
|
+
const DEFAULT_FFMPEG_TIMEOUT_MS = 30 * 60_000;
|
|
10
|
+
const DEFAULT_FFPROBE_TIMEOUT_MS = 2 * 60_000;
|
|
11
|
+
export const H264_MEDIUM = ["-c:v", "libx264", "-preset", "medium", "-crf", "23", "-profile:v", "high", "-level:v", "4.0", "-pix_fmt", "yuv420p"];
|
|
12
|
+
// `-ac 2` downmixes any source to stereo. The native `aac` encoder rejects
|
|
13
|
+
// >2-channel layouts (e.g. 5.1 sources fail with "Unsupported channel layout
|
|
14
|
+
// '6 channels'"), and stereo is the right delivery format for videocity's
|
|
15
|
+
// web/social targets anyway.
|
|
16
|
+
export const AUDIO_OPTS = ["-c:a", "aac", "-b:a", "192k", "-ac", "2"];
|
|
17
|
+
function rationalValue(value) {
|
|
18
|
+
if (typeof value !== "string")
|
|
19
|
+
return null;
|
|
20
|
+
const [numerator, denominator] = value.split("/").map(Number);
|
|
21
|
+
if (!Number.isFinite(numerator) || !Number.isFinite(denominator) || denominator === 0)
|
|
22
|
+
return null;
|
|
23
|
+
return numerator / denominator;
|
|
24
|
+
}
|
|
25
|
+
function rationalParts(value, separator = "/") {
|
|
26
|
+
if (typeof value !== "string")
|
|
27
|
+
return null;
|
|
28
|
+
const parts = value.split(separator);
|
|
29
|
+
if (parts.length !== 2)
|
|
30
|
+
return null;
|
|
31
|
+
const numerator = Number(parts[0]);
|
|
32
|
+
const denominator = Number(parts[1]);
|
|
33
|
+
if (!Number.isSafeInteger(numerator)
|
|
34
|
+
|| !Number.isSafeInteger(denominator)
|
|
35
|
+
|| numerator <= 0
|
|
36
|
+
|| denominator <= 0)
|
|
37
|
+
return null;
|
|
38
|
+
return { numerator, denominator };
|
|
39
|
+
}
|
|
40
|
+
function durationTicks(stream, timeBase, fallbackSeconds) {
|
|
41
|
+
const direct = Number(stream.duration_ts);
|
|
42
|
+
if (Number.isSafeInteger(direct) && direct > 0)
|
|
43
|
+
return direct;
|
|
44
|
+
return Math.max(1, Math.round(fallbackSeconds * timeBase.denominator / timeBase.numerator));
|
|
45
|
+
}
|
|
46
|
+
function rotationDegrees(stream) {
|
|
47
|
+
const sideData = Array.isArray(stream.side_data_list)
|
|
48
|
+
? stream.side_data_list
|
|
49
|
+
: [];
|
|
50
|
+
const sideRotation = sideData
|
|
51
|
+
.map((entry) => Number(entry.rotation))
|
|
52
|
+
.find(Number.isFinite);
|
|
53
|
+
const tags = typeof stream.tags === "object" && stream.tags !== null
|
|
54
|
+
? stream.tags
|
|
55
|
+
: {};
|
|
56
|
+
const taggedRotation = Number(tags.rotate);
|
|
57
|
+
return Math.round(sideRotation ?? (Number.isFinite(taggedRotation) ? taggedRotation : 0));
|
|
58
|
+
}
|
|
59
|
+
function videoProbeStream(stream, fallbackSeconds) {
|
|
60
|
+
if (!stream)
|
|
61
|
+
return null;
|
|
62
|
+
const timeBase = rationalParts(stream.time_base) ?? { numerator: 1, denominator: 1_000 };
|
|
63
|
+
const nominalFrameRate = rationalParts(stream.r_frame_rate);
|
|
64
|
+
const averageFrameRate = rationalParts(stream.avg_frame_rate);
|
|
65
|
+
return {
|
|
66
|
+
index: Number.isSafeInteger(Number(stream.index)) ? Number(stream.index) : 0,
|
|
67
|
+
timeBase,
|
|
68
|
+
durationTicks: durationTicks(stream, timeBase, fallbackSeconds),
|
|
69
|
+
codec: typeof stream.codec_name === "string" ? stream.codec_name : "unknown",
|
|
70
|
+
video: {
|
|
71
|
+
width: Math.max(1, Number(stream.width) || 1),
|
|
72
|
+
height: Math.max(1, Number(stream.height) || 1),
|
|
73
|
+
rotationDegrees: rotationDegrees(stream),
|
|
74
|
+
pixelAspect: rationalParts(stream.sample_aspect_ratio, ":")
|
|
75
|
+
?? { numerator: 1, denominator: 1 },
|
|
76
|
+
...(nominalFrameRate ? { nominalFrameRate } : {}),
|
|
77
|
+
...(averageFrameRate ? { averageFrameRate } : {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function audioProbeStream(stream, fallbackSeconds) {
|
|
82
|
+
if (!stream)
|
|
83
|
+
return null;
|
|
84
|
+
const sampleRateHz = Math.max(1, Number(stream.sample_rate) || 48_000);
|
|
85
|
+
const timeBase = rationalParts(stream.time_base)
|
|
86
|
+
?? { numerator: 1, denominator: sampleRateHz };
|
|
87
|
+
const channels = Math.max(1, Number(stream.channels) || 1);
|
|
88
|
+
const channelLayout = typeof stream.channel_layout === "string"
|
|
89
|
+
? stream.channel_layout
|
|
90
|
+
: channels === 1 ? "mono" : channels === 2 ? "stereo" : `${channels} channels`;
|
|
91
|
+
return {
|
|
92
|
+
index: Number.isSafeInteger(Number(stream.index)) ? Number(stream.index) : 0,
|
|
93
|
+
timeBase,
|
|
94
|
+
durationTicks: durationTicks(stream, timeBase, fallbackSeconds),
|
|
95
|
+
codec: typeof stream.codec_name === "string" ? stream.codec_name : "unknown",
|
|
96
|
+
audio: { sampleRateHz, channels, channelLayout },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function taggedDuration(value) {
|
|
100
|
+
if (typeof value !== "string")
|
|
101
|
+
return null;
|
|
102
|
+
const match = /^(\d+):(\d+):(\d+(?:\.\d+)?)$/.exec(value);
|
|
103
|
+
if (!match)
|
|
104
|
+
return null;
|
|
105
|
+
return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);
|
|
106
|
+
}
|
|
107
|
+
function streamDuration(stream, fallback) {
|
|
108
|
+
if (!stream)
|
|
109
|
+
return 0;
|
|
110
|
+
const direct = Number(stream.duration);
|
|
111
|
+
if (Number.isFinite(direct) && direct > 0)
|
|
112
|
+
return direct;
|
|
113
|
+
const durationTs = Number(stream.duration_ts);
|
|
114
|
+
const timeBase = rationalValue(stream.time_base);
|
|
115
|
+
if (Number.isFinite(durationTs) && timeBase !== null && durationTs * timeBase > 0) {
|
|
116
|
+
return durationTs * timeBase;
|
|
117
|
+
}
|
|
118
|
+
const tags = typeof stream.tags === "object" && stream.tags !== null
|
|
119
|
+
? stream.tags
|
|
120
|
+
: undefined;
|
|
121
|
+
const tagged = taggedDuration(tags?.DURATION ?? tags?.duration);
|
|
122
|
+
return tagged !== null && tagged > 0 ? tagged : fallback;
|
|
123
|
+
}
|
|
124
|
+
function interruptionMessage(processName, error, options, timeoutMs) {
|
|
125
|
+
if (options.signal?.aborted) {
|
|
126
|
+
const reason = options.signal.reason;
|
|
127
|
+
const detail = reason instanceof Error ? reason.message : String(reason ?? "aborted");
|
|
128
|
+
return `${processName} cancelled: ${detail}`;
|
|
129
|
+
}
|
|
130
|
+
const processError = error;
|
|
131
|
+
if (processError.killed && processError.signal) {
|
|
132
|
+
return `${processName} timed out after ${timeoutMs}ms`;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
export async function probeVideo(filePath, options = {}) {
|
|
137
|
+
let stdout;
|
|
138
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FFPROBE_TIMEOUT_MS;
|
|
139
|
+
try {
|
|
140
|
+
const result = await execFileAsync(FFPROBE, [
|
|
141
|
+
"-hide_banner",
|
|
142
|
+
"-v", "error",
|
|
143
|
+
"-print_format", "json",
|
|
144
|
+
"-show_streams",
|
|
145
|
+
"-show_format",
|
|
146
|
+
filePath,
|
|
147
|
+
], {
|
|
148
|
+
encoding: "utf8",
|
|
149
|
+
killSignal: "SIGKILL",
|
|
150
|
+
signal: options.signal,
|
|
151
|
+
timeout: timeoutMs,
|
|
152
|
+
});
|
|
153
|
+
stdout = result.stdout;
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
const interruption = interruptionMessage("ffprobe", error, options, timeoutMs);
|
|
157
|
+
if (interruption)
|
|
158
|
+
throw new Error(`${interruption} for ${filePath}`);
|
|
159
|
+
const e = error;
|
|
160
|
+
const stderr = typeof e.stderr === "string" ? e.stderr : e.stderr?.toString() ?? "";
|
|
161
|
+
const exit = e.code !== undefined ? String(e.code) : "?";
|
|
162
|
+
let size = "missing";
|
|
163
|
+
try {
|
|
164
|
+
const { size: bytes } = await fsp.stat(filePath);
|
|
165
|
+
size = `${bytes} bytes`;
|
|
166
|
+
}
|
|
167
|
+
catch { /* file gone */ }
|
|
168
|
+
throw new Error(`ffprobe failed for ${filePath} (exit ${exit}, ${size}): ${stderr.trim() || e.message}`);
|
|
169
|
+
}
|
|
170
|
+
const data = JSON.parse(stdout);
|
|
171
|
+
const videoStream = data.streams?.find((s) => s.codec_type === "video");
|
|
172
|
+
const width = Number(videoStream?.width ?? 0);
|
|
173
|
+
const height = Number(videoStream?.height ?? 0);
|
|
174
|
+
const duration = Number(data.format?.duration ?? 0);
|
|
175
|
+
const videoDuration = streamDuration(videoStream, duration);
|
|
176
|
+
let fps = 30;
|
|
177
|
+
const rFrameRate = videoStream?.r_frame_rate;
|
|
178
|
+
if (typeof rFrameRate === "string" && rFrameRate.includes("/")) {
|
|
179
|
+
const [num, den] = rFrameRate.split("/").map(Number);
|
|
180
|
+
if (den > 0)
|
|
181
|
+
fps = num / den;
|
|
182
|
+
}
|
|
183
|
+
const avgFps = rationalValue(videoStream?.avg_frame_rate);
|
|
184
|
+
const audioStream = data.streams?.find((s) => s.codec_type === "audio");
|
|
185
|
+
const hasAudio = audioStream !== undefined;
|
|
186
|
+
const audioDuration = audioStream ? streamDuration(audioStream, duration) : null;
|
|
187
|
+
const videoCodec = typeof videoStream?.codec_name === "string" ? videoStream.codec_name : null;
|
|
188
|
+
const audioCodec = typeof audioStream?.codec_name === "string" ? audioStream.codec_name : null;
|
|
189
|
+
return {
|
|
190
|
+
width,
|
|
191
|
+
height,
|
|
192
|
+
duration,
|
|
193
|
+
videoDuration,
|
|
194
|
+
audioDuration,
|
|
195
|
+
fps,
|
|
196
|
+
avgFps,
|
|
197
|
+
hasAudio,
|
|
198
|
+
videoCodec,
|
|
199
|
+
audioCodec,
|
|
200
|
+
videoStream: videoProbeStream(videoStream, videoDuration),
|
|
201
|
+
audioStream: audioProbeStream(audioStream, audioDuration ?? duration),
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
export function summarizeFfmpegStderr(stderr, maxChars = 600) {
|
|
205
|
+
const lines = stderr.split("\n").map((l) => l.trimEnd()).filter(Boolean);
|
|
206
|
+
const meaningful = lines.filter((l) => {
|
|
207
|
+
if (/^ffmpeg version /.test(l))
|
|
208
|
+
return false;
|
|
209
|
+
if (/^\s*built with /.test(l))
|
|
210
|
+
return false;
|
|
211
|
+
if (/^\s*configuration:/.test(l))
|
|
212
|
+
return false;
|
|
213
|
+
if (/^\s*lib\w+\s+\d/.test(l))
|
|
214
|
+
return false;
|
|
215
|
+
if (/^(frame|size)=/.test(l))
|
|
216
|
+
return false;
|
|
217
|
+
return true;
|
|
218
|
+
});
|
|
219
|
+
const tail = meaningful.slice(-15).join("\n");
|
|
220
|
+
if (tail.length <= maxChars)
|
|
221
|
+
return tail;
|
|
222
|
+
if (maxChars <= 0)
|
|
223
|
+
return "";
|
|
224
|
+
const truncationMarker = "\n...\n";
|
|
225
|
+
if (maxChars <= truncationMarker.length)
|
|
226
|
+
return tail.slice(0, maxChars);
|
|
227
|
+
const availableChars = maxChars - truncationMarker.length;
|
|
228
|
+
const headChars = Math.ceil(availableChars / 2);
|
|
229
|
+
const tailChars = availableChars - headChars;
|
|
230
|
+
const suffix = tailChars > 0 ? tail.slice(-tailChars) : "";
|
|
231
|
+
return `${tail.slice(0, headChars)}${truncationMarker}${suffix}`;
|
|
232
|
+
}
|
|
233
|
+
export function withQuietFfmpegArgs(args) {
|
|
234
|
+
const hasLoglevel = args.includes("-loglevel") || args.includes("-v");
|
|
235
|
+
const hasHideBanner = args.includes("-hide_banner");
|
|
236
|
+
const prefix = [];
|
|
237
|
+
if (!hasHideBanner)
|
|
238
|
+
prefix.push("-hide_banner");
|
|
239
|
+
if (!hasLoglevel)
|
|
240
|
+
prefix.push("-loglevel", "error");
|
|
241
|
+
return prefix.length === 0 ? args : [...prefix, ...args];
|
|
242
|
+
}
|
|
243
|
+
export async function runFfmpeg(args, options = {}) {
|
|
244
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_FFMPEG_TIMEOUT_MS;
|
|
245
|
+
try {
|
|
246
|
+
await execFileAsync(FFMPEG, withQuietFfmpegArgs(args), {
|
|
247
|
+
encoding: "utf8",
|
|
248
|
+
killSignal: "SIGKILL",
|
|
249
|
+
signal: options.signal,
|
|
250
|
+
timeout: timeoutMs,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
const interruption = interruptionMessage("ffmpeg", error, options, timeoutMs);
|
|
255
|
+
if (interruption)
|
|
256
|
+
throw new Error(interruption);
|
|
257
|
+
const stderrRaw = error instanceof Error ? error.stderr : undefined;
|
|
258
|
+
const fallback = error instanceof Error ? error.message : String(error);
|
|
259
|
+
const summary = stderrRaw ? summarizeFfmpegStderr(stderrRaw) : "";
|
|
260
|
+
throw new Error(`ffmpeg failed: ${summary || fallback}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
let _FFmpeggy = null;
|
|
264
|
+
const SAMPLE_RATE = 8000;
|
|
265
|
+
// Fine-grained RMS accumulators (~768 KB). We fold these in halves as audio
|
|
266
|
+
// streams in, so memory stays constant regardless of clip length — see
|
|
267
|
+
// extractWaveform.
|
|
268
|
+
const WAVEFORM_FINE_BUCKETS = 1 << 16;
|
|
269
|
+
/**
|
|
270
|
+
* Streams an asset's decoded audio and folds it into a bounded set of RMS
|
|
271
|
+
* accumulators, returning `barCount` normalised peaks (0..1).
|
|
272
|
+
*
|
|
273
|
+
* The earlier implementation buffered the entire PCM stream via execFile with a
|
|
274
|
+
* 50 MB `maxBuffer`. A video whose audio decoded to more than ~52 minutes of
|
|
275
|
+
* 8 kHz mono s16le overflowed that cap; ffmpeg was killed mid-stream, the error
|
|
276
|
+
* was swallowed, and an empty waveform came back — which never cached and so
|
|
277
|
+
* re-ran the multi-second decode on every poll. Streaming removes the cap (and
|
|
278
|
+
* the failure mode) entirely: memory is O(WAVEFORM_FINE_BUCKETS), not
|
|
279
|
+
* O(duration).
|
|
280
|
+
*/
|
|
281
|
+
export async function extractWaveform(videoPath, barCount, streamIndex, signal) {
|
|
282
|
+
signal?.throwIfAborted();
|
|
283
|
+
const sumSq = new Float64Array(WAVEFORM_FINE_BUCKETS);
|
|
284
|
+
const count = new Int32Array(WAVEFORM_FINE_BUCKETS);
|
|
285
|
+
let samplesPerBucket = 1;
|
|
286
|
+
let writeIdx = 0;
|
|
287
|
+
let inBucket = 0;
|
|
288
|
+
let totalSamples = 0;
|
|
289
|
+
// Halve resolution in place when the buckets fill: merge each adjacent pair,
|
|
290
|
+
// double the samples-per-bucket, and resume writing at the midpoint. This
|
|
291
|
+
// caps the accumulators at WAVEFORM_FINE_BUCKETS for any clip length.
|
|
292
|
+
const foldPairs = () => {
|
|
293
|
+
const half = WAVEFORM_FINE_BUCKETS >> 1;
|
|
294
|
+
for (let i = 0; i < half; i++) {
|
|
295
|
+
sumSq[i] = sumSq[2 * i] + sumSq[2 * i + 1];
|
|
296
|
+
count[i] = count[2 * i] + count[2 * i + 1];
|
|
297
|
+
}
|
|
298
|
+
sumSq.fill(0, half);
|
|
299
|
+
count.fill(0, half);
|
|
300
|
+
samplesPerBucket *= 2;
|
|
301
|
+
writeIdx = half;
|
|
302
|
+
};
|
|
303
|
+
const addSample = (sample) => {
|
|
304
|
+
sumSq[writeIdx] += sample * sample;
|
|
305
|
+
inBucket++;
|
|
306
|
+
totalSamples++;
|
|
307
|
+
if (inBucket === samplesPerBucket) {
|
|
308
|
+
count[writeIdx] = inBucket;
|
|
309
|
+
writeIdx++;
|
|
310
|
+
inBucket = 0;
|
|
311
|
+
if (writeIdx === WAVEFORM_FINE_BUCKETS)
|
|
312
|
+
foldPairs();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
try {
|
|
316
|
+
await new Promise((resolve, reject) => {
|
|
317
|
+
const child = spawn(FFMPEG, withQuietFfmpegArgs([
|
|
318
|
+
"-i", videoPath, "-map", streamIndex === undefined ? "0:a:0" : `0:${streamIndex}`, "-vn", "-ac", "1", "-ar", String(SAMPLE_RATE), "-f", "s16le", "pipe:1",
|
|
319
|
+
]), { stdio: ["ignore", "pipe", "pipe"] });
|
|
320
|
+
const abort = () => child.kill("SIGTERM");
|
|
321
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
322
|
+
if (signal?.aborted)
|
|
323
|
+
abort();
|
|
324
|
+
let carry = -1; // a single low byte left over when a chunk splits a sample
|
|
325
|
+
child.stdout.on("data", (chunk) => {
|
|
326
|
+
let offset = 0;
|
|
327
|
+
if (carry !== -1 && chunk.length > 0) {
|
|
328
|
+
addSample((((carry | (chunk[0] << 8)) << 16) >> 16)); // sign-extend 16-bit LE
|
|
329
|
+
offset = 1;
|
|
330
|
+
carry = -1;
|
|
331
|
+
}
|
|
332
|
+
const usableBytes = chunk.length - offset;
|
|
333
|
+
const end = offset + usableBytes - (usableBytes & 1);
|
|
334
|
+
for (let i = offset; i < end; i += 2)
|
|
335
|
+
addSample(chunk.readInt16LE(i));
|
|
336
|
+
carry = usableBytes & 1 ? chunk[chunk.length - 1] : -1;
|
|
337
|
+
});
|
|
338
|
+
child.stderr.resume();
|
|
339
|
+
child.on("error", reject);
|
|
340
|
+
child.on("close", (code) => {
|
|
341
|
+
signal?.removeEventListener("abort", abort);
|
|
342
|
+
// No samples means no decodable audio (no audio stream, or a decode
|
|
343
|
+
// error). Surface it once, but return empty rather than rejecting so a
|
|
344
|
+
// genuinely silent clip caches as "no waveform" instead of churning.
|
|
345
|
+
if (totalSamples === 0 && code !== 0) {
|
|
346
|
+
logger.warn("waveform-empty", { exitCode: code });
|
|
347
|
+
}
|
|
348
|
+
resolve();
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
signal?.throwIfAborted();
|
|
352
|
+
if (totalSamples === 0)
|
|
353
|
+
return [];
|
|
354
|
+
if (inBucket > 0)
|
|
355
|
+
count[writeIdx++] = inBucket; // finalise the partial bucket
|
|
356
|
+
const usedBuckets = writeIdx;
|
|
357
|
+
// Default: 100 bars per second of audio (0.01s resolution), clamped 20..1000.
|
|
358
|
+
if (barCount === undefined) {
|
|
359
|
+
const durationSec = totalSamples / SAMPLE_RATE;
|
|
360
|
+
barCount = Math.max(20, Math.min(1000, Math.round(durationSec * 100)));
|
|
361
|
+
}
|
|
362
|
+
barCount = Math.min(barCount, usedBuckets);
|
|
363
|
+
// True RMS per bar: sum the squared energy and sample counts across the
|
|
364
|
+
// fine buckets the bar spans, then take the root-mean-square.
|
|
365
|
+
const peaks = new Array(barCount);
|
|
366
|
+
for (let b = 0; b < barCount; b++) {
|
|
367
|
+
const start = Math.floor((b * usedBuckets) / barCount);
|
|
368
|
+
const stop = Math.max(start + 1, Math.floor(((b + 1) * usedBuckets) / barCount));
|
|
369
|
+
let energy = 0;
|
|
370
|
+
let samples = 0;
|
|
371
|
+
for (let i = start; i < stop && i < usedBuckets; i++) {
|
|
372
|
+
energy += sumSq[i];
|
|
373
|
+
samples += count[i];
|
|
374
|
+
}
|
|
375
|
+
peaks[b] = samples > 0 ? Math.sqrt(energy / samples) : 0;
|
|
376
|
+
}
|
|
377
|
+
const max = Math.max(...peaks, 1);
|
|
378
|
+
return peaks.map((p) => p / max);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
signal?.throwIfAborted();
|
|
382
|
+
// Only reached if ffmpeg fails to spawn (e.g. binary missing). The stream
|
|
383
|
+
// never buffers, so the old maxBuffer failure mode can't recur here.
|
|
384
|
+
logger.error("waveform-failed", { error: err });
|
|
385
|
+
return [];
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
export async function isThumbnailBlank(imagePath) {
|
|
389
|
+
try {
|
|
390
|
+
const { stdout } = await execFileAsync(FFMPEG, withQuietFfmpegArgs([
|
|
391
|
+
"-i", imagePath,
|
|
392
|
+
"-vf", "format=gray",
|
|
393
|
+
"-f", "rawvideo",
|
|
394
|
+
"-pix_fmt", "gray",
|
|
395
|
+
"pipe:1",
|
|
396
|
+
]), { encoding: "buffer", maxBuffer: 10 * 1024 * 1024 });
|
|
397
|
+
const buf = stdout;
|
|
398
|
+
if (buf.length === 0)
|
|
399
|
+
return true;
|
|
400
|
+
let sum = 0;
|
|
401
|
+
for (let i = 0; i < buf.length; i++)
|
|
402
|
+
sum += buf[i];
|
|
403
|
+
const mean = sum / buf.length;
|
|
404
|
+
let variance = 0;
|
|
405
|
+
for (let i = 0; i < buf.length; i++) {
|
|
406
|
+
const diff = buf[i] - mean;
|
|
407
|
+
variance += diff * diff;
|
|
408
|
+
}
|
|
409
|
+
const stddev = Math.sqrt(variance / buf.length);
|
|
410
|
+
// Near-black or uniform solid color
|
|
411
|
+
return mean < 10 || stddev < 5;
|
|
412
|
+
}
|
|
413
|
+
catch {
|
|
414
|
+
return false;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
export async function getFFmpeggy() {
|
|
418
|
+
if (!_FFmpeggy) {
|
|
419
|
+
const mod = await import("ffmpeggy");
|
|
420
|
+
_FFmpeggy = mod.FFmpeggy;
|
|
421
|
+
_FFmpeggy.DefaultConfig.ffmpegBin = FFMPEG;
|
|
422
|
+
_FFmpeggy.DefaultConfig.ffprobeBin = FFPROBE;
|
|
423
|
+
}
|
|
424
|
+
return _FFmpeggy;
|
|
425
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { accessSync, chmodSync, closeSync, constants, openSync, readSync, statSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
const MACH_O_64_MAGIC_LE = 0xfeedfacf;
|
|
4
|
+
const FAT_MAGIC_BE = 0xcafebabe;
|
|
5
|
+
const CPU_TYPE_X86_64 = 0x01000007;
|
|
6
|
+
const CPU_TYPE_ARM64 = 0x0100000c;
|
|
7
|
+
/** Reads the CPU type from a Mach-O header; null when it is not Mach-O. */
|
|
8
|
+
export function machOArch(header) {
|
|
9
|
+
if (header.length < 8)
|
|
10
|
+
return null;
|
|
11
|
+
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
|
|
12
|
+
if (view.getUint32(0, false) === FAT_MAGIC_BE)
|
|
13
|
+
return "universal";
|
|
14
|
+
if (view.getUint32(0, true) !== MACH_O_64_MAGIC_LE)
|
|
15
|
+
return null;
|
|
16
|
+
const cpuType = view.getUint32(4, true);
|
|
17
|
+
if (cpuType === CPU_TYPE_ARM64)
|
|
18
|
+
return "arm64";
|
|
19
|
+
if (cpuType === CPU_TYPE_X86_64)
|
|
20
|
+
return "x64";
|
|
21
|
+
return "other";
|
|
22
|
+
}
|
|
23
|
+
/** Whether a binary with this header can run on the host CPU. */
|
|
24
|
+
export function binaryRunsOnHost(header, hostArch) {
|
|
25
|
+
const arch = machOArch(header);
|
|
26
|
+
return arch === null || arch === "universal" || arch === hostArch;
|
|
27
|
+
}
|
|
28
|
+
export function readBinaryHeader(path) {
|
|
29
|
+
try {
|
|
30
|
+
const fd = openSync(path, "r");
|
|
31
|
+
try {
|
|
32
|
+
const header = new Uint8Array(8);
|
|
33
|
+
return readSync(fd, header, 0, 8, 0) === 8 ? header : null;
|
|
34
|
+
}
|
|
35
|
+
finally {
|
|
36
|
+
closeSync(fd);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
export function ensureExecutable(path) {
|
|
44
|
+
try {
|
|
45
|
+
accessSync(path, constants.X_OK);
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
try {
|
|
50
|
+
chmodSync(path, statSync(path).mode | 0o111);
|
|
51
|
+
accessSync(path, constants.X_OK);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
export function resolveFfprobePath(bundledPath, override, hostArch) {
|
|
60
|
+
if (override)
|
|
61
|
+
return override;
|
|
62
|
+
if (!bundledPath)
|
|
63
|
+
return "ffprobe";
|
|
64
|
+
const header = readBinaryHeader(bundledPath);
|
|
65
|
+
return header && binaryRunsOnHost(header, hostArch) ? bundledPath : "ffprobe";
|
|
66
|
+
}
|
|
67
|
+
const require = createRequire(import.meta.url);
|
|
68
|
+
export function bundledFfprobe() {
|
|
69
|
+
try {
|
|
70
|
+
const { path } = require("@ffprobe-installer/ffprobe");
|
|
71
|
+
return ensureExecutable(path) ? path : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export const FFPROBE = resolveFfprobePath(bundledFfprobe(), process.env.VIDEOBOOK_FFPROBE_PATH, process.arch);
|