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,133 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { basename, extname } from "node:path";
|
|
5
|
+
import { registerJobHandler } from "../queue.js";
|
|
6
|
+
import { FFPROBE, runFfmpeg } from "../ffmpeg.js";
|
|
7
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
8
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
9
|
+
import { AUDIO_TRIM_EPSILON_SECONDS, validateAudioTrimRange } from "../audio-trim-range.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const LOCK_DURATION_MS = 300_000;
|
|
12
|
+
async function probeAudioDuration(filePath) {
|
|
13
|
+
const { stdout } = await execFileAsync(FFPROBE, [
|
|
14
|
+
"-hide_banner",
|
|
15
|
+
"-v", "error",
|
|
16
|
+
"-print_format", "json",
|
|
17
|
+
"-show_format",
|
|
18
|
+
"-show_streams",
|
|
19
|
+
filePath,
|
|
20
|
+
]);
|
|
21
|
+
const data = JSON.parse(stdout);
|
|
22
|
+
const audioStream = data.streams?.find((stream) => stream.codec_type === "audio");
|
|
23
|
+
if (!audioStream)
|
|
24
|
+
throw new Error("Audio file has no usable audio stream");
|
|
25
|
+
const formatDuration = Number(data.format?.duration ?? 0);
|
|
26
|
+
const streamDuration = Number(audioStream.duration ?? formatDuration);
|
|
27
|
+
const duration = Number.isFinite(streamDuration) && streamDuration > 0
|
|
28
|
+
? streamDuration
|
|
29
|
+
: formatDuration;
|
|
30
|
+
if (!Number.isFinite(duration) || duration <= 0) {
|
|
31
|
+
throw new Error(`Audio file has invalid duration (${duration})`);
|
|
32
|
+
}
|
|
33
|
+
return duration;
|
|
34
|
+
}
|
|
35
|
+
function audioCodecArgs(inputPath) {
|
|
36
|
+
const ext = extname(inputPath).toLowerCase();
|
|
37
|
+
if (ext === ".mp3")
|
|
38
|
+
return ["-c:a", "libmp3lame", "-q:a", "2"];
|
|
39
|
+
if (ext === ".ogg")
|
|
40
|
+
return ["-c:a", "libvorbis", "-q:a", "4"];
|
|
41
|
+
if (ext === ".flac")
|
|
42
|
+
return ["-c:a", "flac"];
|
|
43
|
+
if (ext === ".wav")
|
|
44
|
+
return ["-c:a", "pcm_s16le"];
|
|
45
|
+
return ["-c:a", "aac", "-b:a", "192k"];
|
|
46
|
+
}
|
|
47
|
+
function buildKeepArgs(inputPath, outputPath, start, duration) {
|
|
48
|
+
const args = [];
|
|
49
|
+
if (start > 0)
|
|
50
|
+
args.push("-ss", String(start));
|
|
51
|
+
args.push("-i", inputPath, "-map", "0:a:0", "-vn", "-t", String(duration), ...audioCodecArgs(inputPath), "-y", outputPath);
|
|
52
|
+
return args;
|
|
53
|
+
}
|
|
54
|
+
function buildTrimAudioArgs(inputPath, outputPath, sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd) {
|
|
55
|
+
const expectedDuration = validateAudioTrimRange(sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd);
|
|
56
|
+
if (mode === "keep") {
|
|
57
|
+
return {
|
|
58
|
+
args: buildKeepArgs(inputPath, outputPath, trimStart ?? 0, expectedDuration),
|
|
59
|
+
expectedDuration,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const start = cutStart ?? 0;
|
|
63
|
+
const end = Math.min(cutEnd ?? sourceDuration, sourceDuration);
|
|
64
|
+
if (start <= AUDIO_TRIM_EPSILON_SECONDS) {
|
|
65
|
+
return {
|
|
66
|
+
args: buildKeepArgs(inputPath, outputPath, end, expectedDuration),
|
|
67
|
+
expectedDuration,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
if (end >= sourceDuration - AUDIO_TRIM_EPSILON_SECONDS) {
|
|
71
|
+
return {
|
|
72
|
+
args: buildKeepArgs(inputPath, outputPath, 0, expectedDuration),
|
|
73
|
+
expectedDuration,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const filter = [
|
|
77
|
+
`[0:a]atrim=0:${start},asetpts=PTS-STARTPTS[a1]`,
|
|
78
|
+
`[0:a]atrim=start=${end},asetpts=PTS-STARTPTS[a2]`,
|
|
79
|
+
"[a1][a2]concat=n=2:v=0:a=1[a]",
|
|
80
|
+
].join(";");
|
|
81
|
+
return {
|
|
82
|
+
args: [
|
|
83
|
+
"-i", inputPath,
|
|
84
|
+
"-filter_complex", filter,
|
|
85
|
+
"-map", "[a]",
|
|
86
|
+
...audioCodecArgs(inputPath),
|
|
87
|
+
"-y", outputPath,
|
|
88
|
+
],
|
|
89
|
+
expectedDuration,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function runTrimAudio(inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd) {
|
|
93
|
+
const sourceDuration = await probeAudioDuration(inputPath);
|
|
94
|
+
const { args, expectedDuration } = buildTrimAudioArgs(inputPath, outputPath, sourceDuration, mode, trimStart, trimEnd, cutStart, cutEnd);
|
|
95
|
+
await runFfmpeg(args);
|
|
96
|
+
const outputDuration = await probeAudioDuration(outputPath);
|
|
97
|
+
const tolerance = Math.max(0.35, expectedDuration * 0.05);
|
|
98
|
+
if (Math.abs(outputDuration - expectedDuration) > tolerance) {
|
|
99
|
+
throw new Error(`Trimmed audio duration ${outputDuration.toFixed(3)} did not match expected ${expectedDuration.toFixed(3)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function registerTrimAudioJob(engine) {
|
|
103
|
+
registerJobHandler("trim_audio", async (payload, ctx) => {
|
|
104
|
+
const { inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd, assetId, assetDir, commitParams, } = payload;
|
|
105
|
+
const inputFilename = basename(inputPath);
|
|
106
|
+
try {
|
|
107
|
+
await withLockedLocalLease(engine, assetId, assetDir, ctx, "trim", { durationMs: LOCK_DURATION_MS, data: { state: "trimming-audio", ...commitParams } }, async () => {
|
|
108
|
+
const original = await engine.files.read(assetId, inputFilename);
|
|
109
|
+
if (!original.ok)
|
|
110
|
+
throw new Error(original.error.message);
|
|
111
|
+
await runTrimAudio(inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd);
|
|
112
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
113
|
+
const provenanceResult = await engine.metadata.artifacts.write(assetId, "original", {
|
|
114
|
+
origin: "trim_audio",
|
|
115
|
+
source_asset_id: assetId,
|
|
116
|
+
...commitParams,
|
|
117
|
+
trimmed_at: new Date().toISOString(),
|
|
118
|
+
});
|
|
119
|
+
if (!provenanceResult.ok) {
|
|
120
|
+
const rollback = await engine.files.write(assetId, inputFilename, original.value);
|
|
121
|
+
if (!rollback.ok) {
|
|
122
|
+
throw new Error(`${provenanceResult.error.message}; rollback failed: ${rollback.error.message}`);
|
|
123
|
+
}
|
|
124
|
+
throw new Error(provenanceResult.error.message);
|
|
125
|
+
}
|
|
126
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
127
|
+
return { success: true };
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
await rm(outputPath, { force: true });
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdir, writeFile as fsWriteFile, rename, rm } from "node:fs/promises";
|
|
3
|
+
import { promisify } from "node:util";
|
|
4
|
+
import { join, basename } from "node:path";
|
|
5
|
+
import { registerJobHandler, queue } from "../queue.js";
|
|
6
|
+
import { runFfmpeg, H264_MEDIUM, AUDIO_OPTS, FFPROBE } from "../ffmpeg.js";
|
|
7
|
+
import { withLockedLocalLease } from "./_lease.js";
|
|
8
|
+
import { replaceArtifactFileFromPath } from "./_persist-output.js";
|
|
9
|
+
const execFileAsync = promisify(execFile);
|
|
10
|
+
const LOCK_DURATION_MS = 300_000;
|
|
11
|
+
const CROSSFADE_SECS = 0.03;
|
|
12
|
+
const DURATION_EPSILON_SECS = 0.5;
|
|
13
|
+
// Stream-copy cuts snap to keyframes, so A/V container durations often differ
|
|
14
|
+
// by ~1–2s on long sources. Keep this loose enough to accept those while still
|
|
15
|
+
// rejecting the catastrophic desyncs (e.g. 62s declared with 20s of audio)
|
|
16
|
+
// that prompted this check.
|
|
17
|
+
const AV_DRIFT_TOLERANCE_SECS = 2.0;
|
|
18
|
+
// `ffmpeg -t N` lands on the next frame/sample boundary, so the container's
|
|
19
|
+
// reported format duration is typically 10–50ms longer than N. Pre-shrink by
|
|
20
|
+
// one slack window so the output stays ≤ the requested length — required for
|
|
21
|
+
// downstream caps like Wan 2.7's 10s VideoEdit limit.
|
|
22
|
+
const TRIM_DURATION_SAFETY_SECS = 0.05;
|
|
23
|
+
async function probeStreamDurations(filePath) {
|
|
24
|
+
const { stdout } = await execFileAsync(FFPROBE, [
|
|
25
|
+
"-v", "error",
|
|
26
|
+
"-print_format", "json",
|
|
27
|
+
"-show_streams",
|
|
28
|
+
"-show_format",
|
|
29
|
+
filePath,
|
|
30
|
+
]);
|
|
31
|
+
const data = JSON.parse(stdout);
|
|
32
|
+
const format = Number(data.format?.duration ?? 0);
|
|
33
|
+
const videoStream = data.streams?.find((s) => s.codec_type === "video");
|
|
34
|
+
const audioStream = data.streams?.find((s) => s.codec_type === "audio");
|
|
35
|
+
const video = videoStream ? Number(videoStream.duration ?? format) : null;
|
|
36
|
+
const audio = audioStream ? Number(audioStream.duration ?? format) : null;
|
|
37
|
+
return { format, video, audio };
|
|
38
|
+
}
|
|
39
|
+
export async function assertSaneTrimOutput(inputPath, outputPath) {
|
|
40
|
+
const [input, output] = await Promise.all([
|
|
41
|
+
probeStreamDurations(inputPath),
|
|
42
|
+
probeStreamDurations(outputPath),
|
|
43
|
+
]);
|
|
44
|
+
const video = output.video;
|
|
45
|
+
if (!Number.isFinite(output.format) || output.format <= 0
|
|
46
|
+
|| video === null || !Number.isFinite(video) || video <= 0) {
|
|
47
|
+
throw new Error(`Trim output has no usable video stream (format=${output.format}, video=${video})`);
|
|
48
|
+
}
|
|
49
|
+
if (output.format > input.format + DURATION_EPSILON_SECS) {
|
|
50
|
+
throw new Error(`Trim output duration ${output.format.toFixed(3)}s exceeds input ${input.format.toFixed(3)}s — likely a corrupted concat`);
|
|
51
|
+
}
|
|
52
|
+
if (output.audio !== null && (!Number.isFinite(output.audio)
|
|
53
|
+
|| Math.abs(video - output.audio) > AV_DRIFT_TOLERANCE_SECS)) {
|
|
54
|
+
throw new Error(`Trim output audio/video duration mismatch: video=${video.toFixed(3)}s audio=${output.audio.toFixed(3)}s`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// Write to `${outputPath}.tmp` then rename to outputPath, so a partial write
|
|
58
|
+
// (process killed mid-encode, OOM, etc.) never leaves a moov-less file
|
|
59
|
+
// sitting at the canonical asset path. `+faststart` puts moov at the front,
|
|
60
|
+
// which means even a truncated mp4 is partially playable AND surfaces obvious
|
|
61
|
+
// corruption to the user immediately rather than after first probe.
|
|
62
|
+
//
|
|
63
|
+
// Prefer stream-copy: a mid-point split of a multi-minute source must finish in
|
|
64
|
+
// seconds, not re-encode the whole file with libx264 medium (which times out
|
|
65
|
+
// enqueueAndWait and leaves husk assets with original.mp4.tmp). Cuts snap to
|
|
66
|
+
// keyframes. Fall back to re-encode when copy fails or produces a broken file
|
|
67
|
+
// (no mid-GOP keyframes → severe A/V desync).
|
|
68
|
+
const COPY_AV_DRIFT_TOLERANCE_SECS = 2.0;
|
|
69
|
+
const COPY_DURATION_SLACK_SECS = 2.0;
|
|
70
|
+
export async function trimRange(inputPath, outputPath, start, end) {
|
|
71
|
+
try {
|
|
72
|
+
await runTrimRange(inputPath, outputPath, start, end, "copy");
|
|
73
|
+
await assertCopyTrimSane(inputPath, outputPath, start, end);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
await rm(outputPath, { force: true });
|
|
77
|
+
await rm(`${outputPath}.tmp`, { force: true });
|
|
78
|
+
await runTrimRange(inputPath, outputPath, start, end, "reencode");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async function assertCopyTrimSane(inputPath, outputPath, start, end) {
|
|
82
|
+
const [input, output] = await Promise.all([
|
|
83
|
+
probeStreamDurations(inputPath),
|
|
84
|
+
probeStreamDurations(outputPath),
|
|
85
|
+
]);
|
|
86
|
+
const video = output.video;
|
|
87
|
+
if (!Number.isFinite(output.format) || output.format <= 0
|
|
88
|
+
|| video === null || !Number.isFinite(video) || video <= 0) {
|
|
89
|
+
throw new Error(`Copy trim has no usable video stream (format=${output.format}, video=${video})`);
|
|
90
|
+
}
|
|
91
|
+
if (output.audio !== null
|
|
92
|
+
&& (!Number.isFinite(output.audio)
|
|
93
|
+
|| Math.abs(video - output.audio) > COPY_AV_DRIFT_TOLERANCE_SECS)) {
|
|
94
|
+
throw new Error(`Copy trim A/V desync: video=${video.toFixed(3)}s audio=${output.audio.toFixed(3)}s`);
|
|
95
|
+
}
|
|
96
|
+
let expected;
|
|
97
|
+
if (start !== undefined && end !== undefined)
|
|
98
|
+
expected = end - start;
|
|
99
|
+
else if (end !== undefined)
|
|
100
|
+
expected = end;
|
|
101
|
+
else if (start !== undefined)
|
|
102
|
+
expected = input.format - start;
|
|
103
|
+
if (expected !== undefined && expected > 0) {
|
|
104
|
+
const slack = Math.max(COPY_DURATION_SLACK_SECS, expected * 0.05);
|
|
105
|
+
if (Math.abs(output.format - expected) > slack) {
|
|
106
|
+
throw new Error(`Copy trim duration ${output.format.toFixed(3)}s off expected ${expected.toFixed(3)}s`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async function runTrimRange(inputPath, outputPath, start, end, mode) {
|
|
111
|
+
const args = ["-y"];
|
|
112
|
+
// Copy: put -ss AFTER -i so the cut lands on/after the requested time
|
|
113
|
+
// (before -i keyframe-snaps backward and can keep almost the whole source).
|
|
114
|
+
// Re-encode: put -ss BEFORE -i so the decoder skips work.
|
|
115
|
+
if (mode === "reencode" && start !== undefined) {
|
|
116
|
+
args.push("-ss", String(start));
|
|
117
|
+
}
|
|
118
|
+
args.push("-i", inputPath);
|
|
119
|
+
if (mode === "copy" && start !== undefined) {
|
|
120
|
+
args.push("-ss", String(start));
|
|
121
|
+
}
|
|
122
|
+
if (end !== undefined) {
|
|
123
|
+
const requested = start !== undefined ? end - start : end;
|
|
124
|
+
// Safety margin only for re-encode: shaving 50ms on copy can drop a GOP.
|
|
125
|
+
const duration = mode === "reencode"
|
|
126
|
+
? Math.max(0.01, requested - TRIM_DURATION_SAFETY_SECS)
|
|
127
|
+
: Math.max(0.01, requested);
|
|
128
|
+
args.push("-t", String(duration));
|
|
129
|
+
}
|
|
130
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
131
|
+
if (mode === "copy") {
|
|
132
|
+
args.push("-c", "copy", "-avoid_negative_ts", "make_zero", "-movflags", "+faststart", "-f", "mp4", tmpPath);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
args.push(...H264_MEDIUM, ...AUDIO_OPTS, "-movflags", "+faststart", "-f", "mp4", tmpPath);
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await runFfmpeg(args);
|
|
139
|
+
await rename(tmpPath, outputPath);
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
await rm(tmpPath, { force: true });
|
|
143
|
+
throw err;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Cut exactly `durationSec` seconds starting at `startSec`, with the output's
|
|
148
|
+
* container duration reported as the integer second value (not 9.97 / 10.01).
|
|
149
|
+
*
|
|
150
|
+
* The trick: forcing the output framerate to 24 means a 23.976-fps source
|
|
151
|
+
* (very common — YouTube, most films) becomes 240 frames at 24fps = 10.000s.
|
|
152
|
+
* Audio gets `-t durationSec` directly. With both streams landing on
|
|
153
|
+
* the same integer-second boundary, ffprobe reports `format=duration` as
|
|
154
|
+
* an integer.
|
|
155
|
+
*
|
|
156
|
+
* Used by /even-split where the user wants the first chunk to read as
|
|
157
|
+
* "exactly 10s" / "exactly 9s" — a hard requirement for some video-gen UIs
|
|
158
|
+
* that match on the duration field. No safety margin: precision is the goal.
|
|
159
|
+
*/
|
|
160
|
+
const EXACT_COPY_TOLERANCE_SECS = 0.05;
|
|
161
|
+
export async function trimRangeExact(inputPath, outputPath, startSec, durationSec) {
|
|
162
|
+
try {
|
|
163
|
+
await runTrimRange(inputPath, outputPath, startSec > 0 ? startSec : undefined, startSec + durationSec, "copy");
|
|
164
|
+
const copied = await probeStreamDurations(outputPath);
|
|
165
|
+
if (Number.isFinite(copied.format)
|
|
166
|
+
&& copied.format > 0
|
|
167
|
+
&& copied.format <= durationSec
|
|
168
|
+
&& copied.format >= durationSec - EXACT_COPY_TOLERANCE_SECS) {
|
|
169
|
+
await assertSaneTrimOutput(inputPath, outputPath);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
throw new Error(`Copy trim ${copied.format.toFixed(3)}s is not exact for ${durationSec}s`);
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
await rm(outputPath, { force: true });
|
|
176
|
+
await rm(`${outputPath}.tmp`, { force: true });
|
|
177
|
+
}
|
|
178
|
+
const args = ["-y"];
|
|
179
|
+
if (startSec > 0)
|
|
180
|
+
args.push("-ss", String(startSec));
|
|
181
|
+
args.push("-i", inputPath);
|
|
182
|
+
args.push("-t", String(durationSec));
|
|
183
|
+
args.push("-r", "24", "-vsync", "cfr");
|
|
184
|
+
const tmpPath = `${outputPath}.tmp`;
|
|
185
|
+
args.push(...H264_MEDIUM, ...AUDIO_OPTS, "-movflags", "+faststart", "-f", "mp4", tmpPath);
|
|
186
|
+
try {
|
|
187
|
+
await runFfmpeg(args);
|
|
188
|
+
await rename(tmpPath, outputPath);
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
await rm(tmpPath, { force: true });
|
|
192
|
+
throw err;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
async function cutMiddle(inputPath, outputPath, cutStart, cutEnd) {
|
|
196
|
+
// Degenerate cuts: if the cut starts at 0 or ends at/past input duration,
|
|
197
|
+
// one of the two halves would be empty (`-t 0` or seek-past-end) and the
|
|
198
|
+
// subsequent concat fails with "Output file does not contain any stream".
|
|
199
|
+
// Delegate to a single trimRange call in those cases.
|
|
200
|
+
if (cutStart <= 0) {
|
|
201
|
+
await trimRange(inputPath, outputPath, cutEnd, undefined);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
const { format: inputDuration } = await probeStreamDurations(inputPath);
|
|
205
|
+
if (cutEnd >= inputDuration - DURATION_EPSILON_SECS) {
|
|
206
|
+
await trimRange(inputPath, outputPath, undefined, cutStart);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const tempDir = join(outputPath, "..", ".trim_temp");
|
|
210
|
+
await mkdir(tempDir, { recursive: true });
|
|
211
|
+
try {
|
|
212
|
+
const beforePath = join(tempDir, "before.mp4");
|
|
213
|
+
const afterPath = join(tempDir, "after.mp4");
|
|
214
|
+
const concatList = join(tempDir, "concat.txt");
|
|
215
|
+
// The before/after halves read disjoint ranges of the input and write to
|
|
216
|
+
// different output files, so run them in parallel. Halves cut-middle
|
|
217
|
+
// wall-clock on multi-core hosts.
|
|
218
|
+
const fadeOutStart = Math.max(cutStart - CROSSFADE_SECS, 0);
|
|
219
|
+
await Promise.all([
|
|
220
|
+
runFfmpeg([
|
|
221
|
+
"-y", "-i", inputPath,
|
|
222
|
+
"-t", String(cutStart),
|
|
223
|
+
"-af", `afade=t=out:st=${fadeOutStart}:d=${CROSSFADE_SECS}`,
|
|
224
|
+
...H264_MEDIUM, ...AUDIO_OPTS,
|
|
225
|
+
beforePath,
|
|
226
|
+
]),
|
|
227
|
+
runFfmpeg([
|
|
228
|
+
"-y", "-ss", String(cutEnd),
|
|
229
|
+
"-i", inputPath,
|
|
230
|
+
"-af", `afade=t=in:d=${CROSSFADE_SECS}`,
|
|
231
|
+
...H264_MEDIUM, ...AUDIO_OPTS,
|
|
232
|
+
afterPath,
|
|
233
|
+
]),
|
|
234
|
+
]);
|
|
235
|
+
await fsWriteFile(concatList, `file '${beforePath}'\nfile '${afterPath}'\n`);
|
|
236
|
+
// Re-encode rather than `-c copy`: the two halves are independently-encoded
|
|
237
|
+
// libx264/AAC streams, so their SPS/PPS, GOP boundaries, and AAC priming
|
|
238
|
+
// samples don't line up — a copy-concat produces a container with bogus
|
|
239
|
+
// duration metadata and a truncated audio stream.
|
|
240
|
+
await runFfmpeg([
|
|
241
|
+
"-y", "-f", "concat", "-safe", "0",
|
|
242
|
+
"-i", concatList,
|
|
243
|
+
...H264_MEDIUM, ...AUDIO_OPTS,
|
|
244
|
+
outputPath,
|
|
245
|
+
]);
|
|
246
|
+
}
|
|
247
|
+
finally {
|
|
248
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
export function registerTrimVideoJob(engine) {
|
|
252
|
+
registerJobHandler("trim_video", async (payload, ctx) => {
|
|
253
|
+
const { inputPath, outputPath, mode, trimStart, trimEnd, cutStart, cutEnd, assetId, assetDir, bookId, } = payload;
|
|
254
|
+
const result = await withLockedLocalLease(engine, assetId, assetDir, ctx, "trim", { durationMs: LOCK_DURATION_MS, data: { state: "trimming" } }, async () => {
|
|
255
|
+
if (mode === "cut") {
|
|
256
|
+
await cutMiddle(inputPath, outputPath, cutStart, cutEnd);
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
await trimRange(inputPath, outputPath, trimStart, trimEnd);
|
|
260
|
+
}
|
|
261
|
+
try {
|
|
262
|
+
await assertSaneTrimOutput(inputPath, outputPath);
|
|
263
|
+
}
|
|
264
|
+
catch (err) {
|
|
265
|
+
await rm(outputPath, { force: true });
|
|
266
|
+
throw err;
|
|
267
|
+
}
|
|
268
|
+
const inputFilename = basename(inputPath);
|
|
269
|
+
await replaceArtifactFileFromPath(engine, assetId, inputFilename, outputPath);
|
|
270
|
+
for (const stale of [
|
|
271
|
+
".original.analysis.json",
|
|
272
|
+
".analysis.gpt4otrans.json",
|
|
273
|
+
"elevenlabs.json",
|
|
274
|
+
]) {
|
|
275
|
+
await engine.files.delete(assetId, stale);
|
|
276
|
+
}
|
|
277
|
+
const videoPath = join(assetDir, inputFilename);
|
|
278
|
+
return { videoPath };
|
|
279
|
+
}, { durationMs: LOCK_DURATION_MS, waitForActiveMs: 30_000 });
|
|
280
|
+
await queue.enqueue(`process-video-${assetId}-${Date.now()}`, {
|
|
281
|
+
type: "process_video",
|
|
282
|
+
videoPath: result.videoPath,
|
|
283
|
+
assetId,
|
|
284
|
+
assetDir,
|
|
285
|
+
bookId,
|
|
286
|
+
provenance: { origin: "trim", trimmed_at: new Date().toISOString() },
|
|
287
|
+
force: true,
|
|
288
|
+
});
|
|
289
|
+
return { success: true };
|
|
290
|
+
});
|
|
291
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { createXai } from "../usage-providers.js";
|
|
2
|
+
import { mintOtp } from "@apicity/cost";
|
|
3
|
+
import { ensurePaygateSecret } from "./kie-paygate.js";
|
|
4
|
+
import { assertProviderPayload } from "../provider-prompt-transform.js";
|
|
5
|
+
export const XAI_IMAGE_GENERATIONS_DOT_PATH = "v1.images.generations";
|
|
6
|
+
export const XAI_VIDEO_GENERATIONS_DOT_PATH = "v1.videos.generations";
|
|
7
|
+
export function createXaiPaygatedProvider(apiKey, timeout) {
|
|
8
|
+
return createXai({
|
|
9
|
+
apiKey,
|
|
10
|
+
timeout,
|
|
11
|
+
paygate: { secret: ensurePaygateSecret() },
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
export function mintXaiOtp(dotPath, request, ttlSeconds = 10 * 60) {
|
|
15
|
+
return mintOtp(ensurePaygateSecret(), {
|
|
16
|
+
provider: "xai",
|
|
17
|
+
method: "POST",
|
|
18
|
+
dotPath,
|
|
19
|
+
request: request,
|
|
20
|
+
ttl: ttlSeconds,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export async function callXaiPaid(method, dotPath, request) {
|
|
24
|
+
assertProviderPayload(request, { toolName: dotPath, wire: true });
|
|
25
|
+
return method(request, { otp: mintXaiOtp(dotPath, request) });
|
|
26
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { KEYWORD_FRAME_CACHE_FILENAME, KEYWORD_PLACEMENT_METADATA_KEY, KEYWORD_PLACEMENT_VERSION, keywordCueId, } from "./remotion/keyword-placement.js";
|
|
4
|
+
const regionSchema = z.object({
|
|
5
|
+
kind: z.enum(["face", "text", "graphic"]),
|
|
6
|
+
confidence: z.number().min(0).max(1),
|
|
7
|
+
x: z.number().min(0).max(1),
|
|
8
|
+
y: z.number().min(0).max(1),
|
|
9
|
+
width: z.number().positive().max(1),
|
|
10
|
+
height: z.number().positive().max(1),
|
|
11
|
+
}).strict().refine((region) => region.x + region.width <= 1.001 && region.y + region.height <= 1.001, { message: "occupied region extends beyond the source frame" });
|
|
12
|
+
const cueSchema = z.object({
|
|
13
|
+
cueId: z.string().min(1),
|
|
14
|
+
text: z.string().min(1),
|
|
15
|
+
frameTime: z.number().nonnegative(),
|
|
16
|
+
regions: z.array(regionSchema),
|
|
17
|
+
}).strict();
|
|
18
|
+
const analysisSchema = z.object({
|
|
19
|
+
version: z.literal(KEYWORD_PLACEMENT_VERSION),
|
|
20
|
+
fingerprint: z.string().min(1),
|
|
21
|
+
sourceWidth: z.number().positive(),
|
|
22
|
+
sourceHeight: z.number().positive(),
|
|
23
|
+
frameCacheFilename: z.literal(KEYWORD_FRAME_CACHE_FILENAME),
|
|
24
|
+
analyzedAt: z.string().min(1),
|
|
25
|
+
cues: z.array(cueSchema).min(1),
|
|
26
|
+
}).strict();
|
|
27
|
+
export function keywordPlacementFingerprint(videoFile, transcript, graphics) {
|
|
28
|
+
const transcriptHash = createHash("sha256").update(transcript).digest("hex");
|
|
29
|
+
const payload = {
|
|
30
|
+
version: KEYWORD_PLACEMENT_VERSION,
|
|
31
|
+
video: {
|
|
32
|
+
name: videoFile.name,
|
|
33
|
+
size: videoFile.sizeBytes,
|
|
34
|
+
objectHash: videoFile.objectHash,
|
|
35
|
+
},
|
|
36
|
+
transcriptHash,
|
|
37
|
+
cues: graphics.map((graphic) => ({
|
|
38
|
+
id: keywordCueId(graphic),
|
|
39
|
+
text: graphic.text,
|
|
40
|
+
start: graphic.start,
|
|
41
|
+
end: graphic.end,
|
|
42
|
+
frameTime: graphic.frameTime,
|
|
43
|
+
})),
|
|
44
|
+
};
|
|
45
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
export function parseKeywordPlacementAnalysis(raw) {
|
|
48
|
+
return analysisSchema.parse(raw);
|
|
49
|
+
}
|
|
50
|
+
export async function readKeywordPlacementAnalysis(engine, assetId) {
|
|
51
|
+
const result = await engine.metadata.artifacts.read(assetId, KEYWORD_PLACEMENT_METADATA_KEY);
|
|
52
|
+
if (!result.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const parsed = analysisSchema.safeParse(result.value);
|
|
55
|
+
return parsed.success ? parsed.data : null;
|
|
56
|
+
}
|
|
57
|
+
export async function writeKeywordPlacementAnalysis(engine, assetId, analysis) {
|
|
58
|
+
const parsed = parseKeywordPlacementAnalysis(analysis);
|
|
59
|
+
const result = await engine.metadata.artifacts.write(assetId, KEYWORD_PLACEMENT_METADATA_KEY, parsed);
|
|
60
|
+
if (!result.ok)
|
|
61
|
+
throw new Error(`Failed to persist keyword placement analysis: ${result.error.message}`);
|
|
62
|
+
}
|
|
63
|
+
export async function isKeywordPlacementCacheValid(engine, assetId, analysis, fingerprint, graphics) {
|
|
64
|
+
if (!analysis || analysis.fingerprint !== fingerprint)
|
|
65
|
+
return false;
|
|
66
|
+
const expectedCueIds = graphics.map(keywordCueId);
|
|
67
|
+
const cachedCueIds = analysis.cues.map((cue) => cue.cueId);
|
|
68
|
+
if (expectedCueIds.length !== cachedCueIds.length)
|
|
69
|
+
return false;
|
|
70
|
+
if (expectedCueIds.some((cueId, index) => cueId !== cachedCueIds[index]))
|
|
71
|
+
return false;
|
|
72
|
+
const frameCache = await engine.files.read(assetId, KEYWORD_FRAME_CACHE_FILENAME);
|
|
73
|
+
return frameCache.ok;
|
|
74
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { isSubtitleStyle } from "./remotion/subtitles/styles.js";
|
|
2
|
+
import { isOsgPosition, isOsgSize } from "./osg-assets.js";
|
|
3
|
+
import { queue } from "./queue.js";
|
|
4
|
+
import { readTimeline } from "./timeline-helpers.js";
|
|
5
|
+
import { isLanguageSetting } from "./jobs/_translate-transcript.js";
|
|
6
|
+
import { PLAIN_TRANSCRIPT_FILE, TIMED_TRANSCRIPT_FILE, } from "./transcript-projection.js";
|
|
7
|
+
import { VIDEO_ANALYSIS_FILE } from "./video-analysis-projection.js";
|
|
8
|
+
import { findEligibleRenderActions, readStoredRenderDefaults } from "./tools/reconcile-project-work.js";
|
|
9
|
+
import { DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT, } from "./remotion/keyword-graphics.js";
|
|
10
|
+
function isSubtitlePosition(value) {
|
|
11
|
+
return value === "top" || value === "middle" || value === "bottom";
|
|
12
|
+
}
|
|
13
|
+
function isFontSizePreset(value) {
|
|
14
|
+
return value === "small" || value === "medium" || value === "large";
|
|
15
|
+
}
|
|
16
|
+
function isBackgroundMode(value) {
|
|
17
|
+
return value === "blur" || value === "black";
|
|
18
|
+
}
|
|
19
|
+
function stringOr(value, fallback) {
|
|
20
|
+
return typeof value === "string" ? value : fallback;
|
|
21
|
+
}
|
|
22
|
+
function numberOr(value, fallback) {
|
|
23
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
24
|
+
}
|
|
25
|
+
function nullableString(value) {
|
|
26
|
+
return typeof value === "string" ? value : null;
|
|
27
|
+
}
|
|
28
|
+
function stringArrayOrNull(value) {
|
|
29
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string") ? value : null;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Revive the slot-drop vision analysis for a timeline-assigned video whose
|
|
33
|
+
* .original.analysis.json is missing — the state a force re-process (trim,
|
|
34
|
+
* re-download) leaves behind after it invalidates the analysis sidecars.
|
|
35
|
+
* assign_slot only fires on slot drop, so without this revival the auto
|
|
36
|
+
* pipeline (analyze_video → ElevenLabs transcribe → lazy render) stays wedged
|
|
37
|
+
* and render_timeline reports missing_renders until a manual /render.
|
|
38
|
+
*/
|
|
39
|
+
export async function enqueueLazyAnalysisIfSlotted(engine, bookId, assetId) {
|
|
40
|
+
// Same provider gate as assign_slot's lazy analysis: truthy env check (not
|
|
41
|
+
// getApiKey) so placeholder keys used in tests pass; the real placeholder
|
|
42
|
+
// defense lives at the API call inside analyze-video.
|
|
43
|
+
if (!process.env.OPENAI_API_KEY)
|
|
44
|
+
return null;
|
|
45
|
+
const asset = engine.artifacts.get(assetId);
|
|
46
|
+
if (!asset.ok || asset.value.kind !== "video")
|
|
47
|
+
return null;
|
|
48
|
+
const timeline = readTimeline(engine);
|
|
49
|
+
if (!timeline.slots.some((slot) => slot.artifactId === asset.value.artifactId))
|
|
50
|
+
return null;
|
|
51
|
+
const manifestResult = await engine.files.manifest(assetId);
|
|
52
|
+
if (!manifestResult.ok)
|
|
53
|
+
return null;
|
|
54
|
+
const [analysis, plainTranscript, timedTranscript] = await Promise.all([
|
|
55
|
+
engine.files.read(assetId, VIDEO_ANALYSIS_FILE),
|
|
56
|
+
engine.files.read(assetId, PLAIN_TRANSCRIPT_FILE),
|
|
57
|
+
engine.files.read(assetId, TIMED_TRANSCRIPT_FILE),
|
|
58
|
+
]);
|
|
59
|
+
if (analysis.ok)
|
|
60
|
+
return null;
|
|
61
|
+
// Stable dedupe key: concurrent triggers collapse onto one queued job.
|
|
62
|
+
const jobId = `auto-analyze-${assetId}`;
|
|
63
|
+
await queue.enqueue(jobId, {
|
|
64
|
+
type: "analyze_video",
|
|
65
|
+
assetId,
|
|
66
|
+
assetDir: manifestResult.value.path,
|
|
67
|
+
bookId,
|
|
68
|
+
hasTranscription: plainTranscript.ok || timedTranscript.ok,
|
|
69
|
+
});
|
|
70
|
+
return { jobId };
|
|
71
|
+
}
|
|
72
|
+
async function isRenderAlreadyQueued(engine, assetId, orientation) {
|
|
73
|
+
const work = await engine.jobs.artifactWork.read(assetId);
|
|
74
|
+
if (!work.ok || !work.value)
|
|
75
|
+
return false;
|
|
76
|
+
const state = work.value;
|
|
77
|
+
return state.status !== "ready"
|
|
78
|
+
&& state.meta?.kind === "render"
|
|
79
|
+
&& state.meta.orientation === orientation;
|
|
80
|
+
}
|
|
81
|
+
export async function enqueueLazyTimelineRender(engine, bookId, assetId, knownAsset) {
|
|
82
|
+
const resolved = engine.artifacts.get(assetId);
|
|
83
|
+
if (!resolved.ok || resolved.value.kind !== "video")
|
|
84
|
+
return null;
|
|
85
|
+
const timeline = readTimeline(engine);
|
|
86
|
+
if (!timeline.slots.some((slot) => slot.artifactId === resolved.value.artifactId))
|
|
87
|
+
return null;
|
|
88
|
+
const orientation = timeline.render;
|
|
89
|
+
if (await isRenderAlreadyQueued(engine, assetId, orientation))
|
|
90
|
+
return null;
|
|
91
|
+
const asset = knownAsset ?? resolved.value;
|
|
92
|
+
const renderActions = await findEligibleRenderActions(engine, [asset], orientation);
|
|
93
|
+
if (renderActions.length === 0)
|
|
94
|
+
return null;
|
|
95
|
+
const action = renderActions[0];
|
|
96
|
+
const stored = await readStoredRenderDefaults(engine, assetId, orientation);
|
|
97
|
+
const subtitlesEnabled = action.hasElevenlabs && stored.subtitlesEnabled === true;
|
|
98
|
+
// Stable dedupe key: concurrent triggers (job-completion hooks racing a
|
|
99
|
+
// sync poll) collapse onto one queued render instead of double-rendering.
|
|
100
|
+
const jobId = `auto-render-${assetId}-${orientation}`;
|
|
101
|
+
await queue.enqueue(jobId, {
|
|
102
|
+
type: "render_project",
|
|
103
|
+
bookId,
|
|
104
|
+
assetId: action.assetId,
|
|
105
|
+
orientation,
|
|
106
|
+
assets: [{
|
|
107
|
+
assetId: action.assetId,
|
|
108
|
+
assetDir: action.assetDir,
|
|
109
|
+
videoPath: action.videoPath,
|
|
110
|
+
videoFilename: action.videoFilename,
|
|
111
|
+
}],
|
|
112
|
+
subtitlesEnabled,
|
|
113
|
+
subtitleLanguage: isLanguageSetting(stored.subtitleLanguage) ? stored.subtitleLanguage : "original",
|
|
114
|
+
highlightStyle: isSubtitleStyle(stored.highlightStyle) ? stored.highlightStyle : "highlight",
|
|
115
|
+
position: isSubtitlePosition(stored.position) ? stored.position : "bottom",
|
|
116
|
+
fontSize: isFontSizePreset(stored.fontSize) ? stored.fontSize : "medium",
|
|
117
|
+
textColor: stringOr(stored.textColor, "#FFFFFF"),
|
|
118
|
+
highlightColor: stringOr(stored.highlightColor, "#991B1B"),
|
|
119
|
+
audioFadeIn: numberOr(stored.audioFadeIn, 0),
|
|
120
|
+
audioFadeOut: numberOr(stored.audioFadeOut, 0),
|
|
121
|
+
audioVolume: numberOr(stored.audioVolume, 100),
|
|
122
|
+
osgImagePath: nullableString(stored.osgImagePath),
|
|
123
|
+
osgAssetId: nullableString(stored.osgAssetId),
|
|
124
|
+
osgPosition: isOsgPosition(stored.osgPosition) ? stored.osgPosition : "top-right",
|
|
125
|
+
osgSize: isOsgSize(stored.osgSize) ? stored.osgSize : "medium",
|
|
126
|
+
osgScale: numberOr(stored.osgScale, 1),
|
|
127
|
+
osgOpacity: numberOr(stored.osgOpacity, 1),
|
|
128
|
+
osgStartSeconds: typeof stored.osgStartSeconds === "number" ? stored.osgStartSeconds : null,
|
|
129
|
+
osgEndSeconds: typeof stored.osgEndSeconds === "number" ? stored.osgEndSeconds : null,
|
|
130
|
+
osgTailSeconds: typeof stored.osgTailSeconds === "number" ? stored.osgTailSeconds : null,
|
|
131
|
+
keywordGraphicsEnabled: stored.keywordGraphicsEnabled === true,
|
|
132
|
+
keywordGraphicsMaxCount: numberOr(stored.keywordGraphicsMaxCount, DEFAULT_KEYWORD_GRAPHICS_MAX_COUNT),
|
|
133
|
+
keywordGraphicsLingerSeconds: numberOr(stored.keywordGraphicsLingerSeconds, DEFAULT_KEYWORD_GRAPHICS_LINGER_SECONDS),
|
|
134
|
+
keywordGraphicsKeywords: stringArrayOrNull(stored.keywordGraphicsKeywords),
|
|
135
|
+
backgroundMode: isBackgroundMode(stored.backgroundMode) ? stored.backgroundMode : "blur",
|
|
136
|
+
strokeWidth: numberOr(stored.strokeWidth, 6),
|
|
137
|
+
strokeColor: stringOr(stored.strokeColor, "#000000"),
|
|
138
|
+
shadowBlur: numberOr(stored.shadowBlur, 6),
|
|
139
|
+
shadowOpacity: numberOr(stored.shadowOpacity, 60),
|
|
140
|
+
});
|
|
141
|
+
return { jobId };
|
|
142
|
+
}
|