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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The queued tagging coordinator.
|
|
3
|
+
*
|
|
4
|
+
* One job type tags every kind of asset, because the hard parts — deciding
|
|
5
|
+
* which content was analyzed, refusing to publish a stale result, and leaving
|
|
6
|
+
* a user's own tags alone — are the same everywhere. What differs is only
|
|
7
|
+
* where the evidence lives, and that is a lookup.
|
|
8
|
+
*
|
|
9
|
+
* Three properties this job must hold:
|
|
10
|
+
*
|
|
11
|
+
* - **Detached from readiness.** Tagging never touches the artifact work row.
|
|
12
|
+
* A book whose provider key is missing still plays its media; only its tags
|
|
13
|
+
* are absent, and the outcome says so.
|
|
14
|
+
* - **Fenced to the content.** The fence is the primary media file's hash, not
|
|
15
|
+
* the asset's whole-directory fingerprint, which changes whenever a sidecar
|
|
16
|
+
* is written — including the analysis and tag writes this job itself causes.
|
|
17
|
+
* Fencing on the directory would make every run trigger the next one.
|
|
18
|
+
* - **Idempotent.** The job id carries book, artifact, source hash and
|
|
19
|
+
* analyzer version, so a restart, a duplicate completion event and a second
|
|
20
|
+
* enqueue collapse into one run; a forced retry opts out explicitly.
|
|
21
|
+
*/
|
|
22
|
+
import { findAudioFile, findPrimaryMediaFile } from "videobook-engine";
|
|
23
|
+
import { getApiKey } from "../api-key.js";
|
|
24
|
+
import { logger } from "../logger.js";
|
|
25
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
26
|
+
import { tagsFromImageAnalysis, tagsFromScriptText, tagsFromTranscript, tagsFromVideoAnalysis, } from "../asset-tag-extraction.js";
|
|
27
|
+
import { readTaggingSettings } from "../tag-settings.js";
|
|
28
|
+
import { TAG_ANALYZER_VERSION } from "../tag-vocabulary.js";
|
|
29
|
+
const ANALYSIS_FILENAME = ".original.analysis.json";
|
|
30
|
+
/**
|
|
31
|
+
* Kinds that carry something automatic tagging can read. A character sheet or
|
|
32
|
+
* a prompt is not untagged because analysis failed — there is nothing about it
|
|
33
|
+
* to analyze, and manual tags remain available on every kind.
|
|
34
|
+
*/
|
|
35
|
+
const TAGGABLE_KINDS = new Set([
|
|
36
|
+
"image",
|
|
37
|
+
"video",
|
|
38
|
+
"final",
|
|
39
|
+
"audio",
|
|
40
|
+
"script",
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* The bytes a tag describes. Rendered orientations and sidecars are excluded:
|
|
44
|
+
* a tag is a claim about the content someone sees, and re-rendering does not
|
|
45
|
+
* change that content.
|
|
46
|
+
*/
|
|
47
|
+
async function primarySourceHash(engine, assetId, kind) {
|
|
48
|
+
const manifest = await engine.files.manifest(assetId);
|
|
49
|
+
if (!manifest.ok)
|
|
50
|
+
return null;
|
|
51
|
+
const files = manifest.value.files;
|
|
52
|
+
if (kind === "script") {
|
|
53
|
+
const document = files.find((file) => !file.name.startsWith("."));
|
|
54
|
+
return document?.objectHash ?? null;
|
|
55
|
+
}
|
|
56
|
+
const primary = kind === "audio"
|
|
57
|
+
? findAudioFile(files, kind) ?? findPrimaryMediaFile(files, kind)
|
|
58
|
+
: findPrimaryMediaFile(files, kind);
|
|
59
|
+
return primary?.objectHash ?? null;
|
|
60
|
+
}
|
|
61
|
+
async function readAnalysis(engine, assetId) {
|
|
62
|
+
const read = await engine.files.read(assetId, ANALYSIS_FILENAME);
|
|
63
|
+
if (!read.ok)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
return JSON.parse(read.value.toString("utf8"));
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
async function transcriptEvidence(engine, assetId) {
|
|
73
|
+
const transcripts = await engine.transcripts.list(assetId);
|
|
74
|
+
const latest = transcripts.ok ? transcripts.value.at(-1) : undefined;
|
|
75
|
+
if (!latest)
|
|
76
|
+
return null;
|
|
77
|
+
const segments = latest.segments ?? [];
|
|
78
|
+
const wordCount = segments.reduce((total, segment) => total + segment.text.trim().split(/\s+/u).filter(Boolean).length, 0);
|
|
79
|
+
const speakers = new Set(segments.flatMap((segment) => (segment.speaker ? [segment.speaker] : [])));
|
|
80
|
+
return { wordCount, speakerCount: speakers.size };
|
|
81
|
+
}
|
|
82
|
+
async function readScriptText(engine, assetId) {
|
|
83
|
+
const manifest = await engine.files.manifest(assetId);
|
|
84
|
+
if (!manifest.ok)
|
|
85
|
+
return null;
|
|
86
|
+
const document = manifest.value.files.find((file) => /\.(md|txt|fountain)$/iu.test(file.name));
|
|
87
|
+
if (!document)
|
|
88
|
+
return null;
|
|
89
|
+
const read = await engine.files.read(assetId, document.name);
|
|
90
|
+
return read.ok ? read.value.toString("utf8") : null;
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Which evidence a kind carries. Audio has no frames, images have no
|
|
94
|
+
* transcript, and a kind with neither is not a failure — it simply has
|
|
95
|
+
* nothing automatic tagging can read.
|
|
96
|
+
*/
|
|
97
|
+
async function extractForKind(engine, assetId, kind) {
|
|
98
|
+
if (kind === "image") {
|
|
99
|
+
return tagsFromImageAnalysis(await readAnalysis(engine, assetId));
|
|
100
|
+
}
|
|
101
|
+
if (kind === "video" || kind === "final") {
|
|
102
|
+
const fromFrames = tagsFromVideoAnalysis(await readAnalysis(engine, assetId));
|
|
103
|
+
if (fromFrames.outcome === "tagged")
|
|
104
|
+
return fromFrames;
|
|
105
|
+
// A video whose analysis said nothing useful can still be grouped by what
|
|
106
|
+
// its transcript supports.
|
|
107
|
+
const fromSpeech = tagsFromTranscript(await transcriptEvidence(engine, assetId));
|
|
108
|
+
return fromSpeech.outcome === "tagged" ? fromSpeech : fromFrames;
|
|
109
|
+
}
|
|
110
|
+
if (kind === "audio") {
|
|
111
|
+
return tagsFromTranscript(await transcriptEvidence(engine, assetId));
|
|
112
|
+
}
|
|
113
|
+
if (kind === "script") {
|
|
114
|
+
return tagsFromScriptText(await readScriptText(engine, assetId));
|
|
115
|
+
}
|
|
116
|
+
return { tags: [], outcome: "unsupported_kind" };
|
|
117
|
+
}
|
|
118
|
+
/** Deterministic id: the same content and analyzer never queue twice. */
|
|
119
|
+
export function tagAssetJobId(bookId, assetId, sourceHash) {
|
|
120
|
+
return `tag-asset-${bookId}-${assetId}-${sourceHash}-${TAG_ANALYZER_VERSION}`;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Queues tagging for one asset. Callers are completion tails — upload,
|
|
124
|
+
* processing, generation, analysis — so this must never throw into them: an
|
|
125
|
+
* asset that cannot be tagged is still a usable asset.
|
|
126
|
+
*/
|
|
127
|
+
export async function enqueueAssetTagging(engine, assetId, options = {}) {
|
|
128
|
+
try {
|
|
129
|
+
if (!options.force && !readTaggingSettings(engine).automaticEnabled)
|
|
130
|
+
return;
|
|
131
|
+
const bookId = engine.book.get().bookId;
|
|
132
|
+
const artifact = engine.artifacts.get(assetId);
|
|
133
|
+
if (!artifact.ok)
|
|
134
|
+
return;
|
|
135
|
+
const sourceHash = await primarySourceHash(engine, assetId, artifact.value.kind);
|
|
136
|
+
if (!sourceHash)
|
|
137
|
+
return;
|
|
138
|
+
await queue.enqueue(tagAssetJobId(bookId, assetId, sourceHash), {
|
|
139
|
+
type: "tag_asset",
|
|
140
|
+
bookId,
|
|
141
|
+
assetId,
|
|
142
|
+
...(options.force ? { force: true } : {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
catch (cause) {
|
|
146
|
+
logger.warn("tag-asset-failed", { assetId, error: cause });
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
export async function runAssetTagging(engine, payload, control) {
|
|
150
|
+
const { assetId, force } = payload;
|
|
151
|
+
const base = { assetId, tagCount: 0, analyzerVersion: TAG_ANALYZER_VERSION };
|
|
152
|
+
const artifact = engine.artifacts.get(assetId);
|
|
153
|
+
if (!artifact.ok) {
|
|
154
|
+
return { ...base, outcome: "missing_asset", sourceHash: null };
|
|
155
|
+
}
|
|
156
|
+
if (!TAGGABLE_KINDS.has(artifact.value.kind)) {
|
|
157
|
+
return { ...base, outcome: "unsupported_kind", sourceHash: null };
|
|
158
|
+
}
|
|
159
|
+
const sourceHash = await primarySourceHash(engine, assetId, artifact.value.kind);
|
|
160
|
+
if (!sourceHash) {
|
|
161
|
+
return { ...base, outcome: "no_source_content", sourceHash: null };
|
|
162
|
+
}
|
|
163
|
+
// The engine keeps the last accepted snapshot. Re-running over the same
|
|
164
|
+
// content with the same analyzer would spend a write to reach the same
|
|
165
|
+
// state, and would clear nothing a user has since dismissed.
|
|
166
|
+
const snapshot = engine.tags.automatic.snapshot(assetId);
|
|
167
|
+
const current = snapshot.ok ? snapshot.value : undefined;
|
|
168
|
+
if (!force
|
|
169
|
+
&& current
|
|
170
|
+
&& current.sourceHash === sourceHash
|
|
171
|
+
&& current.extractorVersion === TAG_ANALYZER_VERSION
|
|
172
|
+
&& !current.stale) {
|
|
173
|
+
return { ...base, outcome: "already_current", sourceHash };
|
|
174
|
+
}
|
|
175
|
+
const extraction = await extractForKind(engine, assetId, artifact.value.kind);
|
|
176
|
+
if (extraction.outcome === "unsupported_kind") {
|
|
177
|
+
return { ...base, outcome: extraction.outcome, sourceHash };
|
|
178
|
+
}
|
|
179
|
+
if (extraction.outcome === "no_evidence") {
|
|
180
|
+
if (payload.cacheOnly) {
|
|
181
|
+
// Nothing usable is cached. Saying so is the point of a cache-only pass:
|
|
182
|
+
// the asset is counted as needing analysis rather than claimed current.
|
|
183
|
+
return { ...base, outcome: "no_evidence", sourceHash };
|
|
184
|
+
}
|
|
185
|
+
// Analysis is otherwise gated on dropping an asset into a timeline, which
|
|
186
|
+
// would leave everything that stays in the Library untagged. Ask for it
|
|
187
|
+
// here instead; the analysis tail re-queues this job, and the dedupe key
|
|
188
|
+
// is free again by then because this run has finished.
|
|
189
|
+
const requested = await requestAnalysis(engine, payload.bookId, assetId, artifact.value.kind, control);
|
|
190
|
+
return { ...base, outcome: requested, sourceHash };
|
|
191
|
+
}
|
|
192
|
+
// Re-read the hash: the content may have been replaced while the evidence
|
|
193
|
+
// was being read, and publishing against the old bytes would describe media
|
|
194
|
+
// the asset no longer has.
|
|
195
|
+
const publishHash = await primarySourceHash(engine, assetId, artifact.value.kind);
|
|
196
|
+
if (publishHash !== sourceHash) {
|
|
197
|
+
return { ...base, outcome: "no_source_content", sourceHash: publishHash };
|
|
198
|
+
}
|
|
199
|
+
const replaced = await engine.tags.automatic.replace({
|
|
200
|
+
artifactId: assetId,
|
|
201
|
+
sourceHash,
|
|
202
|
+
generator: "videobook-editorial-tagger",
|
|
203
|
+
extractorVersion: TAG_ANALYZER_VERSION,
|
|
204
|
+
tags: extraction.tags,
|
|
205
|
+
});
|
|
206
|
+
if (!replaced.ok) {
|
|
207
|
+
logger.warn("tag-asset-failed", { assetId, error: replaced.error });
|
|
208
|
+
return { ...base, outcome: extraction.outcome, sourceHash };
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
...base,
|
|
212
|
+
outcome: extraction.outcome,
|
|
213
|
+
tagCount: extraction.tags.length,
|
|
214
|
+
sourceHash,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Asks for the evidence this kind needs. Nothing here waits: the analysis job
|
|
219
|
+
* caches its result and re-queues tagging, so a missing provider key costs an
|
|
220
|
+
* outcome rather than a failed job on a perfectly good asset.
|
|
221
|
+
*/
|
|
222
|
+
async function requestAnalysis(engine, bookId, assetId, kind, control) {
|
|
223
|
+
if (control && !control.canRequestAnalysis())
|
|
224
|
+
return "no_evidence";
|
|
225
|
+
if (kind !== "image" && kind !== "video" && kind !== "final")
|
|
226
|
+
return "no_evidence";
|
|
227
|
+
if (!readTaggingSettings(engine).analysisOnNewAssets)
|
|
228
|
+
return "no_evidence";
|
|
229
|
+
if (!getApiKey("OPENAI_API_KEY"))
|
|
230
|
+
return "analysis_unavailable";
|
|
231
|
+
const manifest = await engine.files.manifest(assetId);
|
|
232
|
+
if (!manifest.ok)
|
|
233
|
+
return "no_evidence";
|
|
234
|
+
if (control && !control.canRequestAnalysis())
|
|
235
|
+
return "no_evidence";
|
|
236
|
+
if (kind === "image") {
|
|
237
|
+
const original = manifest.value.files.find((file) => /^original\.(jpe?g|png|webp)$/iu.test(file.name));
|
|
238
|
+
if (!original)
|
|
239
|
+
return "no_evidence";
|
|
240
|
+
await queue.enqueue(`auto-describe-${assetId}`, {
|
|
241
|
+
type: "describe_image",
|
|
242
|
+
bookId,
|
|
243
|
+
sourceAssetId: assetId,
|
|
244
|
+
sourceImageFilename: original.name,
|
|
245
|
+
});
|
|
246
|
+
return "analysis_requested";
|
|
247
|
+
}
|
|
248
|
+
if (kind === "video" || kind === "final") {
|
|
249
|
+
await queue.enqueue(`auto-analyze-${assetId}`, {
|
|
250
|
+
type: "analyze_video",
|
|
251
|
+
bookId,
|
|
252
|
+
assetId,
|
|
253
|
+
assetDir: manifest.value.path,
|
|
254
|
+
hasTranscription: false,
|
|
255
|
+
// Tagging wants the analysis and nothing else: an asset nobody has put
|
|
256
|
+
// on a timeline must not pull transcription and rendering along behind
|
|
257
|
+
// it just because it was tagged.
|
|
258
|
+
purpose: "tagging",
|
|
259
|
+
});
|
|
260
|
+
return "analysis_requested";
|
|
261
|
+
}
|
|
262
|
+
return "no_evidence";
|
|
263
|
+
}
|
|
264
|
+
export function registerTagAssetJob(engine) {
|
|
265
|
+
registerJobHandler("tag_asset", async (payload) => await runAssetTagging(engine, payload));
|
|
266
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { appendDiagnostic } from "../diagnostics.js";
|
|
2
|
+
/**
|
|
3
|
+
* CAS-fail a pending task by (asset_id, task_id) and log to the project's
|
|
4
|
+
* action log. Required everywhere that fails a provider task: the task_id
|
|
5
|
+
* scopes the CAS so a late failure of an old task can't clobber a fresh
|
|
6
|
+
* retry that already replaced the row.
|
|
7
|
+
*/
|
|
8
|
+
export async function failPendingTaskAndLog(engine, assetId, taskId, info) {
|
|
9
|
+
const result = await engine.jobs.pending.fail(assetId, taskId, info);
|
|
10
|
+
if (!result.ok)
|
|
11
|
+
return;
|
|
12
|
+
await appendDiagnostic(engine, "job-error", {
|
|
13
|
+
assetId,
|
|
14
|
+
error: info,
|
|
15
|
+
}).catch(() => { });
|
|
16
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { rm } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { probeVideo, runFfmpeg, H264_MEDIUM, AUDIO_OPTS } from "../ffmpeg.js";
|
|
4
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
5
|
+
import { buildTimewarpVideoFilter, createTimewarpPlan, } from "../timewarp.js";
|
|
6
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
7
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
8
|
+
const LOCK_DURATION_MS = 900_000;
|
|
9
|
+
const STALE_VISUAL_DERIVATIVES = [
|
|
10
|
+
".original.analysis.json",
|
|
11
|
+
".detect-scenes.json",
|
|
12
|
+
".suggest-cuts.json",
|
|
13
|
+
".suggest-sfx.json",
|
|
14
|
+
".analyze-jokes.json",
|
|
15
|
+
];
|
|
16
|
+
function outputArgs(inputPath, outputPath, plan, hasAudio, audioMode) {
|
|
17
|
+
return [
|
|
18
|
+
"-y",
|
|
19
|
+
"-i", inputPath,
|
|
20
|
+
"-filter_complex", buildTimewarpVideoFilter(plan),
|
|
21
|
+
"-map", "[vout]",
|
|
22
|
+
...(hasAudio ? ["-map", "0:a:0"] : ["-an"]),
|
|
23
|
+
...H264_MEDIUM,
|
|
24
|
+
"-pix_fmt", "yuv420p",
|
|
25
|
+
...(hasAudio ? (audioMode === "copy" ? ["-c:a", "copy"] : AUDIO_OPTS) : []),
|
|
26
|
+
"-t", plan.sourceDuration.toFixed(9),
|
|
27
|
+
"-movflags", "+faststart",
|
|
28
|
+
"-f", "mp4",
|
|
29
|
+
outputPath,
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
export async function renderTimewarpedVideo(inputPath, outputPath, plan, hasAudio, signal) {
|
|
33
|
+
if (!hasAudio) {
|
|
34
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, false, "copy"), { signal });
|
|
35
|
+
return "none";
|
|
36
|
+
}
|
|
37
|
+
const source = await probeVideo(inputPath, { signal });
|
|
38
|
+
if (source.audioCodec !== "aac") {
|
|
39
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, true, "aac"), { signal });
|
|
40
|
+
return "aac";
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, true, "copy"), { signal });
|
|
44
|
+
return "copy";
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
await runFfmpeg(outputArgs(inputPath, outputPath, plan, true, "aac"), { signal });
|
|
48
|
+
return "aac";
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export async function validateTimewarpOutput(inputPath, outputPath, plan) {
|
|
52
|
+
const [input, output] = await Promise.all([
|
|
53
|
+
probeVideo(inputPath),
|
|
54
|
+
probeVideo(outputPath),
|
|
55
|
+
]);
|
|
56
|
+
if (output.videoCodec === null || output.width <= 0 || output.height <= 0) {
|
|
57
|
+
throw new Error("Timewarp output has no usable video stream");
|
|
58
|
+
}
|
|
59
|
+
if (output.width !== input.width || output.height !== input.height) {
|
|
60
|
+
throw new Error(`Timewarp output dimensions ${output.width}x${output.height} do not match source ${input.width}x${input.height}`);
|
|
61
|
+
}
|
|
62
|
+
if (output.hasAudio !== input.hasAudio) {
|
|
63
|
+
throw new Error("Timewarp output audio stream layout does not match the source");
|
|
64
|
+
}
|
|
65
|
+
const tolerance = Math.max(0.08, 2 / plan.sourceFps);
|
|
66
|
+
if (!Number.isFinite(output.duration)
|
|
67
|
+
|| Math.abs(output.duration - plan.sourceDuration) > tolerance) {
|
|
68
|
+
throw new Error(`Timewarp output duration ${output.duration.toFixed(3)}s does not match source ${plan.sourceDuration.toFixed(3)}s`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function deleteIfPresent(engine, assetId, filename) {
|
|
72
|
+
const result = await engine.files.delete(assetId, filename);
|
|
73
|
+
if (!result.ok && result.error.code !== "NOT_FOUND") {
|
|
74
|
+
throw new Error(`Failed to delete ${filename}: ${result.error.message}`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function discardTemp(outputPath) {
|
|
78
|
+
await rm(outputPath, { force: true });
|
|
79
|
+
}
|
|
80
|
+
export function registerTimewarpVideoJob(engine) {
|
|
81
|
+
registerJobHandler("timewarp_video", async (payload, ctx) => {
|
|
82
|
+
const { inputPath, outputPath, startSeconds, endSeconds, stretchFactor, assetId, assetDir, bookId, } = payload;
|
|
83
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "change_speed", { durationMs: LOCK_DURATION_MS, data: { state: "timewarping" } }, async () => {
|
|
84
|
+
const source = await probeVideo(inputPath, { signal: ctx.signal });
|
|
85
|
+
const plan = createTimewarpPlan({
|
|
86
|
+
sourceDuration: source.duration,
|
|
87
|
+
sourceFps: source.fps,
|
|
88
|
+
startSeconds,
|
|
89
|
+
endSeconds,
|
|
90
|
+
stretchFactor,
|
|
91
|
+
});
|
|
92
|
+
let audioMode;
|
|
93
|
+
try {
|
|
94
|
+
audioMode = await renderTimewarpedVideo(inputPath, outputPath, plan, source.hasAudio, ctx.signal);
|
|
95
|
+
await validateTimewarpOutput(inputPath, outputPath, plan);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
await discardTemp(outputPath);
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
const history = engine.history.artifact(assetId);
|
|
102
|
+
const restoreCommit = history[0]?.hash;
|
|
103
|
+
try {
|
|
104
|
+
const inputFilename = basename(inputPath);
|
|
105
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
106
|
+
for (const filename of STALE_VISUAL_DERIVATIVES) {
|
|
107
|
+
await deleteIfPresent(engine, assetId, filename);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (restoreCommit)
|
|
112
|
+
await engine.history.restoreArtifact(assetId, restoreCommit);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
videoPath: join(assetDir, basename(inputPath)),
|
|
117
|
+
plan,
|
|
118
|
+
audioMode,
|
|
119
|
+
};
|
|
120
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
121
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
122
|
+
type: "process_video",
|
|
123
|
+
videoPath: result.videoPath,
|
|
124
|
+
assetId,
|
|
125
|
+
assetDir,
|
|
126
|
+
bookId,
|
|
127
|
+
provenance: {
|
|
128
|
+
origin: "timewarp",
|
|
129
|
+
start_seconds: result.plan.startSeconds,
|
|
130
|
+
end_seconds: result.plan.endSeconds,
|
|
131
|
+
stretch_factor: result.plan.stretchFactor,
|
|
132
|
+
outside_stretch_factor: result.plan.outsideStretchFactor,
|
|
133
|
+
source_duration: result.plan.sourceDuration,
|
|
134
|
+
selected_output_duration: result.plan.selectedOutputDuration,
|
|
135
|
+
audio_preserved: true,
|
|
136
|
+
audio_mode: result.audioMode,
|
|
137
|
+
timewarped_at: new Date().toISOString(),
|
|
138
|
+
},
|
|
139
|
+
force: true,
|
|
140
|
+
skipDownstreamJobs: true,
|
|
141
|
+
preserveAudioArtifacts: true,
|
|
142
|
+
reviveVisualAnalysis: true,
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
success: true,
|
|
146
|
+
source_duration: result.plan.sourceDuration,
|
|
147
|
+
selected_output_duration: result.plan.selectedOutputDuration,
|
|
148
|
+
outside_stretch_factor: result.plan.outsideStretchFactor,
|
|
149
|
+
audio_mode: result.audioMode,
|
|
150
|
+
};
|
|
151
|
+
});
|
|
152
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { createOpenAi } from "../usage-providers.js";
|
|
2
|
+
import { appendDiagnostic } from "../diagnostics.js";
|
|
3
|
+
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { extname, join } from "node:path";
|
|
6
|
+
import { logger } from "../logger.js";
|
|
7
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
8
|
+
import { getApiKey } from "../api-key.js";
|
|
9
|
+
import { probeVideo } from "../ffmpeg.js";
|
|
10
|
+
import { DOWNSTREAM_LEASE_WAIT_MS, withLocalLease, } from "./_lease.js";
|
|
11
|
+
import { chunkStarts, chunkLength, extractAudioChunk } from "./_audio-chunks.js";
|
|
12
|
+
import { enqueueLazyAnalysisIfSlotted, enqueueLazyTimelineRender } from "../lazy-render.js";
|
|
13
|
+
const CHUNK_SECONDS = 5 * 60;
|
|
14
|
+
async function transcribeBytes(provider, bytes, filename) {
|
|
15
|
+
const file = new File([bytes], filename, { type: audioMimeType(filename) });
|
|
16
|
+
const result = await provider.post.v1.audio.transcriptions({
|
|
17
|
+
file,
|
|
18
|
+
model: "gpt-transcribe",
|
|
19
|
+
});
|
|
20
|
+
return result.text;
|
|
21
|
+
}
|
|
22
|
+
async function transcribeChunked(provider, mediaPath, duration) {
|
|
23
|
+
const dir = await mkdtemp(join(tmpdir(), "videocity-transcribe-"));
|
|
24
|
+
try {
|
|
25
|
+
const texts = [];
|
|
26
|
+
const starts = chunkStarts(duration, CHUNK_SECONDS);
|
|
27
|
+
for (let index = 0; index < starts.length; index++) {
|
|
28
|
+
const start = starts[index];
|
|
29
|
+
const length = chunkLength(duration, start, CHUNK_SECONDS);
|
|
30
|
+
const chunkName = `chunk-${String(index).padStart(3, "0")}.mp3`;
|
|
31
|
+
const chunkPath = join(dir, chunkName);
|
|
32
|
+
await extractAudioChunk(mediaPath, start, length, chunkPath);
|
|
33
|
+
const bytes = await readFile(chunkPath);
|
|
34
|
+
texts.push(await transcribeBytes(provider, new Uint8Array(bytes), chunkName));
|
|
35
|
+
}
|
|
36
|
+
return texts.map((text) => text.trim()).filter(Boolean).join(" ");
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
await rm(dir, { recursive: true, force: true }).catch(() => undefined);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function runTranscribeVideoAudio(engine, payload, ctx) {
|
|
43
|
+
const { videoPath, assetId, bookId } = payload;
|
|
44
|
+
const apiKey = getApiKey("OPENAI_API_KEY");
|
|
45
|
+
if (!apiKey)
|
|
46
|
+
return { success: false, reason: "OPENAI_API_KEY not set" };
|
|
47
|
+
const result = await withLocalLease(engine, assetId, ctx, "transcribe", async () => {
|
|
48
|
+
try {
|
|
49
|
+
const provider = createOpenAi({ apiKey, timeout: 120_000 });
|
|
50
|
+
const probe = await probeVideo(videoPath);
|
|
51
|
+
let text;
|
|
52
|
+
let chunkCount = 1;
|
|
53
|
+
if (probe.duration > CHUNK_SECONDS) {
|
|
54
|
+
chunkCount = chunkStarts(probe.duration, CHUNK_SECONDS).length;
|
|
55
|
+
text = await transcribeChunked(provider, videoPath, probe.duration);
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
const sourceFilename = payload.sourceFilename ?? "original.mp3";
|
|
59
|
+
const readResult = await engine.files.read(assetId, sourceFilename);
|
|
60
|
+
if (!readResult.ok)
|
|
61
|
+
throw new Error(readResult.error.message);
|
|
62
|
+
text = await transcribeBytes(provider, new Uint8Array(readResult.value), sourceFilename);
|
|
63
|
+
}
|
|
64
|
+
const jsonString = JSON.stringify({ text }, null, 2);
|
|
65
|
+
await engine.files.write(assetId, ".analysis.gpt4otrans.json", jsonString);
|
|
66
|
+
await queue.enqueue(`semantic-index-${assetId}-transcript-${Date.now()}`, {
|
|
67
|
+
type: "semantic_index",
|
|
68
|
+
assetId,
|
|
69
|
+
bookId,
|
|
70
|
+
});
|
|
71
|
+
return { success: true, text, chunk_count: chunkCount };
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
// Soft-fail: this job is enqueued eagerly after every video with audio
|
|
75
|
+
// (process-video.ts). A failed transcript must not mark the asset
|
|
76
|
+
// 'error' — the video itself is fine and the transcript is a sidecar,
|
|
77
|
+
// same rationale as the waveform soft-fail in process-video.ts.
|
|
78
|
+
// Returning (instead of throwing) lets withLocalLease complete the
|
|
79
|
+
// lease, so the asset stays claimable for a later retry.
|
|
80
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
81
|
+
logger.warn("transcribe-soft-fail", { assetId, error: err });
|
|
82
|
+
await appendDiagnostic(engine, "transcribe-soft-fail", { assetId, error: err }).catch(() => undefined);
|
|
83
|
+
return { success: false, reason };
|
|
84
|
+
}
|
|
85
|
+
}, { waitForActiveMs: DOWNSTREAM_LEASE_WAIT_MS });
|
|
86
|
+
// A force re-process (trim, re-download) invalidated the vision analysis;
|
|
87
|
+
// for slotted assets re-run it here — after speech-to-text, so the analyze
|
|
88
|
+
// prompt includes the fresh transcript — which revives auto-transcribe and
|
|
89
|
+
// the lazy render below (that call skips while the analysis is missing).
|
|
90
|
+
try {
|
|
91
|
+
await enqueueLazyAnalysisIfSlotted(engine, bookId, assetId);
|
|
92
|
+
}
|
|
93
|
+
catch (err) {
|
|
94
|
+
void err;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
await enqueueLazyTimelineRender(engine, bookId, assetId);
|
|
98
|
+
}
|
|
99
|
+
catch (err) {
|
|
100
|
+
void err;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
export function registerTranscribeVideoAudioJob(engine) {
|
|
105
|
+
registerJobHandler("transcribe_video_audio", async (payload, ctx) => runTranscribeVideoAudio(engine, payload, ctx));
|
|
106
|
+
}
|
|
107
|
+
function audioMimeType(filename) {
|
|
108
|
+
switch (extname(filename).toLowerCase()) {
|
|
109
|
+
case ".wav": return "audio/wav";
|
|
110
|
+
case ".ogg": return "audio/ogg";
|
|
111
|
+
case ".weba": return "audio/webm";
|
|
112
|
+
case ".m4a": return "audio/mp4";
|
|
113
|
+
case ".aac": return "audio/aac";
|
|
114
|
+
case ".flac": return "audio/flac";
|
|
115
|
+
default: return "audio/mpeg";
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { queue, registerJobHandler } from "../queue.js";
|
|
2
|
+
import { DOWNSTREAM_LEASE_WAIT_MS, withLocalLease, } from "./_lease.js";
|
|
3
|
+
import { transcribeAssetToElevenLabsJson } from "./_transcribe.js";
|
|
4
|
+
import { enqueueLazyAnalysisIfSlotted, enqueueLazyTimelineRender, } from "../lazy-render.js";
|
|
5
|
+
export function registerTranscribeJob(engine) {
|
|
6
|
+
registerJobHandler("transcribe", async (payload, ctx) => {
|
|
7
|
+
const { audioFilename, tagAudioEvents, diarize, languageCode, assetId, bookId, } = payload;
|
|
8
|
+
const result = await withLocalLease(engine, assetId, ctx, "transcribe", async () => {
|
|
9
|
+
await transcribeAssetToElevenLabsJson(engine, assetId, audioFilename, {
|
|
10
|
+
tagAudioEvents,
|
|
11
|
+
diarize,
|
|
12
|
+
...(languageCode ? { languageCode } : {}),
|
|
13
|
+
});
|
|
14
|
+
return { success: true, provider: "elevenlabs" };
|
|
15
|
+
}, { waitForActiveMs: DOWNSTREAM_LEASE_WAIT_MS });
|
|
16
|
+
await queue.enqueue(`semantic-index-${assetId}-transcript-${Date.now()}`, {
|
|
17
|
+
type: "semantic_index",
|
|
18
|
+
assetId,
|
|
19
|
+
bookId,
|
|
20
|
+
});
|
|
21
|
+
try {
|
|
22
|
+
await enqueueLazyAnalysisIfSlotted(engine, bookId, assetId);
|
|
23
|
+
}
|
|
24
|
+
catch (err) {
|
|
25
|
+
void err;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
await enqueueLazyTimelineRender(engine, bookId, assetId);
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
void err;
|
|
32
|
+
}
|
|
33
|
+
return result;
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { registerJobHandler } from "../queue.js";
|
|
2
|
+
import { requireApiKey } from "../api-key.js";
|
|
3
|
+
import { buildIntelligenceResponses } from "../openai-intelligence.js";
|
|
4
|
+
import { withLocalLease } from "./_lease.js";
|
|
5
|
+
import { enqueueLazyTimelineRender } from "../lazy-render.js";
|
|
6
|
+
import { parseSegments } from "../remotion/subtitles/segments.js";
|
|
7
|
+
import { iso1FromIso3, readLanguageCode, sidecarMatchesSource, translatedTranscriptFilename, translateTranscript, } from "./_translate-transcript.js";
|
|
8
|
+
export async function runTranslateTranscript(engine, payload, ctx) {
|
|
9
|
+
const { assetId, bookId, targetLanguage } = payload;
|
|
10
|
+
// Leased as "transcribe": the engine's ArtifactWorkKind union has no
|
|
11
|
+
// translate kind, and sharing it serializes translation against
|
|
12
|
+
// re-transcription of the same asset.
|
|
13
|
+
const result = await withLocalLease(engine, assetId, ctx, "transcribe", async () => {
|
|
14
|
+
const readResult = await engine.files.read(assetId, "elevenlabs.json");
|
|
15
|
+
if (!readResult.ok) {
|
|
16
|
+
throw new Error(`No transcript for ${assetId}: ${readResult.error.message}. Transcribe the asset first.`);
|
|
17
|
+
}
|
|
18
|
+
const raw = JSON.parse(readResult.value.toString());
|
|
19
|
+
if (iso1FromIso3(readLanguageCode(raw)) === targetLanguage) {
|
|
20
|
+
return { success: true, targetLanguage, skipped: "matches-source" };
|
|
21
|
+
}
|
|
22
|
+
const sidecarFilename = translatedTranscriptFilename(targetLanguage);
|
|
23
|
+
const existing = await engine.files.read(assetId, sidecarFilename);
|
|
24
|
+
if (existing.ok) {
|
|
25
|
+
const sourceSegments = parseSegments(raw);
|
|
26
|
+
try {
|
|
27
|
+
const sidecar = JSON.parse(existing.value.toString());
|
|
28
|
+
if (sidecarMatchesSource(sidecar, sourceSegments)) {
|
|
29
|
+
return { success: true, targetLanguage, cached: true };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// unreadable sidecar — re-translate below
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
const responses = buildIntelligenceResponses(requireApiKey("OPENAI_API_KEY"));
|
|
37
|
+
const sidecar = await translateTranscript(raw, targetLanguage, responses, ctx.signal);
|
|
38
|
+
const writeResult = await engine.files.write(assetId, sidecarFilename, JSON.stringify(sidecar, null, 2));
|
|
39
|
+
if (!writeResult.ok) {
|
|
40
|
+
throw new Error(`Failed to write ${sidecarFilename}: ${writeResult.error.message}`);
|
|
41
|
+
}
|
|
42
|
+
await engine.history.recordOperation("translate_transcript", assetId, { targetLanguage });
|
|
43
|
+
return { success: true, targetLanguage };
|
|
44
|
+
});
|
|
45
|
+
try {
|
|
46
|
+
await enqueueLazyTimelineRender(engine, bookId, assetId);
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
void err;
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
export function registerTranslateTranscriptJob(engine) {
|
|
54
|
+
registerJobHandler("translate_transcript", (payload, ctx) => runTranslateTranscript(engine, payload, ctx));
|
|
55
|
+
}
|