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,670 @@
|
|
|
1
|
+
import { findVideoFile } from "videobook-engine";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { WATERMARK_PLATFORMS } from "../watermark-platform.js";
|
|
5
|
+
import { getApiKey } from "../api-key.js";
|
|
6
|
+
import { probeVideo } from "../ffmpeg.js";
|
|
7
|
+
import { sampleEvenly } from "../frame-utils.js";
|
|
8
|
+
import { MAX_WATERMARK_FADE_SECONDS, } from "../jobs/burn-watermark.js";
|
|
9
|
+
import { resolveOsgImagePath } from "../osg-assets.js";
|
|
10
|
+
import { osgPlacementFingerprint, readOsgPlacementAnalysis, } from "../osg-placement-cache.js";
|
|
11
|
+
import { writeOsgSettings } from "../osg-settings.js";
|
|
12
|
+
import { logger } from "../logger.js";
|
|
13
|
+
import { queue } from "../queue.js";
|
|
14
|
+
import { scoreWatermarkCornersForCanvas, WATERMARK_MIN_DIM_FRACTION, WATERMARK_OSG_SIZE, watermarkOsgScale, } from "../remotion/osg-spec.js";
|
|
15
|
+
import { createWatermarkAsset, DEFAULT_HANDLE_PADDING, DEFAULT_HANDLE_ROUNDING, defaultUrl, findWatermarkAsset, MAX_HANDLE_PADDING, MAX_HANDLE_ROUNDING, MIN_HANDLE_PADDING, MIN_HANDLE_ROUNDING, QR_ONLY_BADGE_ASPECT, watermarkBadgeAspect, } from "../watermark-badge.js";
|
|
16
|
+
import { LIGHTNING_FADE_SECONDS, watermarkProfileToSave, watermarkWindowsFor } from "../lightning-defaults.js";
|
|
17
|
+
import { effectiveWatermarkProfile, normalizeHandle, readWatermarkConfig, recordHandle, saveWatermarkProfile } from "../watermark-config.js";
|
|
18
|
+
import { effectiveWatermarkTiming } from "../watermark-timing.js";
|
|
19
|
+
import { MAX_HANDLE_VERTICAL_PERCENT, MIN_HANDLE_VERTICAL_PERCENT, WATERMARK_HANDLE_PLACEMENTS, } from "../watermark-handle-position.js";
|
|
20
|
+
import { requireArtifactKind } from "./_shared.js";
|
|
21
|
+
import { waitForAssetReadyDef } from "./wait-for-asset-ready.js";
|
|
22
|
+
const PLACEMENT_TIMEOUT_MS = 300_000;
|
|
23
|
+
const BURN_TIMEOUT_MS = 600_000;
|
|
24
|
+
const MAX_PLACEMENT_FRAMES = 8;
|
|
25
|
+
const WATERMARK_POSITIONS = [
|
|
26
|
+
"top-left", "top-center", "top-right",
|
|
27
|
+
"middle-left", "middle-center", "middle-right",
|
|
28
|
+
"bottom-left", "bottom-center", "bottom-right",
|
|
29
|
+
];
|
|
30
|
+
const WATERMARK_POSITION_OPTIONS = ["auto", ...WATERMARK_POSITIONS];
|
|
31
|
+
const ANALYSIS_OSG_POSITIONS = new Set(["top-left", "top-right", "bottom-left", "bottom-right"]);
|
|
32
|
+
const DEFAULT_POSITION = "bottom-right";
|
|
33
|
+
function uniformPositions(position) {
|
|
34
|
+
return { landscape: position, portrait: position, square: position };
|
|
35
|
+
}
|
|
36
|
+
function watermarkTimingError(fieldPrefix, startSeconds, endSeconds) {
|
|
37
|
+
const start = startSeconds ?? 0;
|
|
38
|
+
return endSeconds !== undefined && endSeconds <= start
|
|
39
|
+
? `${fieldPrefix}_end_seconds must be greater than ${fieldPrefix}_start_seconds`
|
|
40
|
+
: null;
|
|
41
|
+
}
|
|
42
|
+
function watermarkVariantForApply(mode, includeQr, includeHandle) {
|
|
43
|
+
if (includeQr && !includeHandle)
|
|
44
|
+
return "qr";
|
|
45
|
+
if (!includeQr)
|
|
46
|
+
return "handle";
|
|
47
|
+
return mode === "burn" ? "qr" : "combined";
|
|
48
|
+
}
|
|
49
|
+
async function resolveWatermarkAsset(engine, handle, url, variant, appearance) {
|
|
50
|
+
const options = { variant, ...appearance };
|
|
51
|
+
const existing = await findWatermarkAsset(engine, handle, url, options);
|
|
52
|
+
if (existing)
|
|
53
|
+
return { ok: true, assetId: existing };
|
|
54
|
+
return createWatermarkAsset(engine, handle, url, options);
|
|
55
|
+
}
|
|
56
|
+
/** Coarse fallback: the corner analyze-video recommended for this clip. */
|
|
57
|
+
async function readRecommendedOsgPosition(engine, assetId) {
|
|
58
|
+
const result = await engine.files.read(assetId, ".original.analysis.json");
|
|
59
|
+
if (!result.ok)
|
|
60
|
+
return null;
|
|
61
|
+
try {
|
|
62
|
+
const raw = typeof result.value === "string"
|
|
63
|
+
? result.value
|
|
64
|
+
: new TextDecoder().decode(result.value);
|
|
65
|
+
const analysis = JSON.parse(raw);
|
|
66
|
+
const position = analysis.recommendedOsgPosition;
|
|
67
|
+
return typeof position === "string" && ANALYSIS_OSG_POSITIONS.has(position)
|
|
68
|
+
? position
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function listPlacementFrames(engine, assetId) {
|
|
76
|
+
const framesResult = await engine.files.listSubdir(assetId, "original_frames");
|
|
77
|
+
if (!framesResult.ok)
|
|
78
|
+
return [];
|
|
79
|
+
return sampleEvenly(framesResult.value.filter((f) => f.endsWith(".jpg")).sort((a, b) => parseFloat(a) - parseFloat(b)), MAX_PLACEMENT_FRAMES);
|
|
80
|
+
}
|
|
81
|
+
function placementFingerprint(videoFile, frameNames, scaling) {
|
|
82
|
+
return osgPlacementFingerprint(videoFile, frameNames, {
|
|
83
|
+
aspect: scaling.aspect,
|
|
84
|
+
minDimFraction: WATERMARK_MIN_DIM_FRACTION,
|
|
85
|
+
userScale: scaling.userScale,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Overlay placement: per-orientation corners. Never fails on placement
|
|
90
|
+
* problems — degrade: cache → vision job → analyze-video recommendation →
|
|
91
|
+
* bottom-right.
|
|
92
|
+
*/
|
|
93
|
+
async function resolveOverlayPlacement(engine, bookId, assetId, assetDir, videoFile, position, scaling) {
|
|
94
|
+
if (position !== "auto") {
|
|
95
|
+
return { positions: uniformPositions(position), placementSource: "explicit" };
|
|
96
|
+
}
|
|
97
|
+
const frameNames = await listPlacementFrames(engine, assetId);
|
|
98
|
+
const fingerprint = placementFingerprint(videoFile, frameNames, scaling);
|
|
99
|
+
const cached = await readOsgPlacementAnalysis(engine, assetId);
|
|
100
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
101
|
+
return { positions: cached.positions, placementSource: "cached" };
|
|
102
|
+
}
|
|
103
|
+
if (getApiKey("OPENAI_API_KEY") && frameNames.length > 0) {
|
|
104
|
+
const jobId = `osg-placement-${assetId}-${Date.now()}`;
|
|
105
|
+
try {
|
|
106
|
+
await engine.jobs.failures.clear(assetId);
|
|
107
|
+
const result = await queue.enqueueAndWait(jobId, {
|
|
108
|
+
type: "osg_placement",
|
|
109
|
+
assetId,
|
|
110
|
+
assetDir,
|
|
111
|
+
bookId,
|
|
112
|
+
fingerprint,
|
|
113
|
+
userScale: scaling.userScale,
|
|
114
|
+
badgeAspect: scaling.aspect,
|
|
115
|
+
}, PLACEMENT_TIMEOUT_MS);
|
|
116
|
+
return { positions: result.analysis.positions, placementSource: "vision", jobId };
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.warn("watermark-placement-fallback", { assetId, error: err });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const recommended = await readRecommendedOsgPosition(engine, assetId);
|
|
123
|
+
return recommended
|
|
124
|
+
? { positions: uniformPositions(recommended), placementSource: "analysis" }
|
|
125
|
+
: { positions: uniformPositions(DEFAULT_POSITION), placementSource: "default" };
|
|
126
|
+
}
|
|
127
|
+
async function scoreCachedCorner(videoPath, analysis, scaling, fallback) {
|
|
128
|
+
// Score corners against the source frame itself — a burned watermark
|
|
129
|
+
// lives in source pixels, so one corner serves every orientation.
|
|
130
|
+
try {
|
|
131
|
+
const probe = await probeVideo(videoPath);
|
|
132
|
+
return scoreWatermarkCornersForCanvas(probe.width, probe.height, analysis.regions, scaling).position;
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
logger.warn("watermark-probe-fallback", { error: err });
|
|
136
|
+
return fallback;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Burn placement: one corner. Cached analyses resolve here; a fresh vision
|
|
141
|
+
* pass is handed to the burn job so the tool can return before it runs, with
|
|
142
|
+
* the analyze-video recommendation (or bottom-right) as its fallback.
|
|
143
|
+
*/
|
|
144
|
+
async function resolveBurnPlacement(engine, assetId, videoFile, videoPath, position, scaling) {
|
|
145
|
+
if (position !== "auto") {
|
|
146
|
+
return { corner: position, placementSource: "explicit", visionRequest: null };
|
|
147
|
+
}
|
|
148
|
+
const frameNames = await listPlacementFrames(engine, assetId);
|
|
149
|
+
const fingerprint = placementFingerprint(videoFile, frameNames, scaling);
|
|
150
|
+
const cached = await readOsgPlacementAnalysis(engine, assetId);
|
|
151
|
+
if (cached && cached.fingerprint === fingerprint) {
|
|
152
|
+
return {
|
|
153
|
+
corner: await scoreCachedCorner(videoPath, cached, scaling, DEFAULT_POSITION),
|
|
154
|
+
placementSource: "cached",
|
|
155
|
+
visionRequest: null,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const recommended = await readRecommendedOsgPosition(engine, assetId);
|
|
159
|
+
const visionAvailable = Boolean(getApiKey("OPENAI_API_KEY")) && frameNames.length > 0;
|
|
160
|
+
return {
|
|
161
|
+
corner: recommended ?? DEFAULT_POSITION,
|
|
162
|
+
placementSource: recommended ? "analysis" : "default",
|
|
163
|
+
visionRequest: visionAvailable ? { fingerprint, badgeAspect: scaling.aspect } : null,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
export function applyWatermarkDef(engine) {
|
|
167
|
+
return {
|
|
168
|
+
name: "apply_watermark",
|
|
169
|
+
description: "Burn a positioned QR code and/or an X or TikTok handle onto a video in place, each with its own opacity, padding and visibility window. wait=false returns once the burn is queued. mode=overlay stores a legacy render-time overlay instead.",
|
|
170
|
+
schema: {
|
|
171
|
+
book_id: z.string().describe("Book id"),
|
|
172
|
+
asset_id: z.string().describe("Video asset id to watermark"),
|
|
173
|
+
platform: z.enum(WATERMARK_PLATFORMS).optional().describe("Alias platform (default tiktok)"),
|
|
174
|
+
handle: z.string().optional().describe("X or TikTok alias without @ (default: saved alias for the platform)"),
|
|
175
|
+
url: z.string().optional().describe("URL the QR code resolves to (default: the selected X or TikTok profile URL)"),
|
|
176
|
+
save_default: z
|
|
177
|
+
.boolean()
|
|
178
|
+
.optional()
|
|
179
|
+
.describe("Remember this handle as the default for future watermarks"),
|
|
180
|
+
include_qr: z
|
|
181
|
+
.boolean()
|
|
182
|
+
.optional()
|
|
183
|
+
.describe("Include a separately positioned QR watermark (default true); false applies only the centered handle"),
|
|
184
|
+
include_handle: z
|
|
185
|
+
.boolean()
|
|
186
|
+
.optional()
|
|
187
|
+
.describe("Include the handle watermark (default true); false applies only the QR"),
|
|
188
|
+
position: z
|
|
189
|
+
.enum(WATERMARK_POSITION_OPTIONS)
|
|
190
|
+
.optional()
|
|
191
|
+
.describe("QR placement anchor. A fresh TikTok profile uses bottom-right; X uses auto"),
|
|
192
|
+
handle_vertical_percent: z
|
|
193
|
+
.number()
|
|
194
|
+
.min(MIN_HANDLE_VERTICAL_PERCENT)
|
|
195
|
+
.max(MAX_HANDLE_VERTICAL_PERCENT)
|
|
196
|
+
.optional()
|
|
197
|
+
.describe("Burn-mode centered-handle centerline as percent from the top (fresh TikTok 10, X 33; range 10-90). Ignored when handle_position attaches the handle to the QR"),
|
|
198
|
+
handle_position: z
|
|
199
|
+
.enum(WATERMARK_HANDLE_PLACEMENTS)
|
|
200
|
+
.optional()
|
|
201
|
+
.describe("Burn-mode handle placement: center (default), above-qr, on-qr, or below-qr"),
|
|
202
|
+
include_platform_icon: z
|
|
203
|
+
.boolean()
|
|
204
|
+
.optional()
|
|
205
|
+
.describe("Show the selected platform icon before the plain alias (default true)"),
|
|
206
|
+
include_handle_border: z
|
|
207
|
+
.boolean()
|
|
208
|
+
.optional()
|
|
209
|
+
.describe("Draw a border around the handle pill (default true)"),
|
|
210
|
+
handle_padding: z
|
|
211
|
+
.number()
|
|
212
|
+
.int()
|
|
213
|
+
.min(MIN_HANDLE_PADDING)
|
|
214
|
+
.max(MAX_HANDLE_PADDING)
|
|
215
|
+
.optional()
|
|
216
|
+
.describe(`Horizontal handle padding in reference pixels (default ${DEFAULT_HANDLE_PADDING}; range ${MIN_HANDLE_PADDING}-${MAX_HANDLE_PADDING})`),
|
|
217
|
+
handle_padding_y: z
|
|
218
|
+
.number()
|
|
219
|
+
.int()
|
|
220
|
+
.min(MIN_HANDLE_PADDING)
|
|
221
|
+
.max(MAX_HANDLE_PADDING)
|
|
222
|
+
.optional()
|
|
223
|
+
.describe(`Vertical handle padding in reference pixels (default ${DEFAULT_HANDLE_PADDING}; range ${MIN_HANDLE_PADDING}-${MAX_HANDLE_PADDING})`),
|
|
224
|
+
handle_rounding: z
|
|
225
|
+
.number()
|
|
226
|
+
.int()
|
|
227
|
+
.min(MIN_HANDLE_ROUNDING)
|
|
228
|
+
.max(MAX_HANDLE_ROUNDING)
|
|
229
|
+
.optional()
|
|
230
|
+
.describe(`Handle corner rounding percent (default ${DEFAULT_HANDLE_ROUNDING}; range ${MIN_HANDLE_ROUNDING}-${MAX_HANDLE_ROUNDING})`),
|
|
231
|
+
qr_start_seconds: z
|
|
232
|
+
.number()
|
|
233
|
+
.min(0)
|
|
234
|
+
.optional()
|
|
235
|
+
.describe("First second where the QR is visible (default clip start)"),
|
|
236
|
+
qr_end_seconds: z
|
|
237
|
+
.number()
|
|
238
|
+
.min(0)
|
|
239
|
+
.optional()
|
|
240
|
+
.describe("Last second where the QR is visible (default clip end)"),
|
|
241
|
+
handle_start_seconds: z
|
|
242
|
+
.number()
|
|
243
|
+
.min(0)
|
|
244
|
+
.optional()
|
|
245
|
+
.describe("First second where the handle is visible (default clip start)"),
|
|
246
|
+
handle_end_seconds: z
|
|
247
|
+
.number()
|
|
248
|
+
.min(0)
|
|
249
|
+
.optional()
|
|
250
|
+
.describe("Last second where the handle is visible (default clip end)"),
|
|
251
|
+
fade_seconds: z
|
|
252
|
+
.number()
|
|
253
|
+
.min(0)
|
|
254
|
+
.max(MAX_WATERMARK_FADE_SECONDS)
|
|
255
|
+
.optional()
|
|
256
|
+
.describe(`Burn-mode fade in/out length in seconds at a watermark edge that falls inside the clip (fresh TikTok ${LIGHTNING_FADE_SECONDS}, X 0.5; 0 keeps hard edges). Whole-clip watermarks never fade`),
|
|
257
|
+
qr_opacity: z
|
|
258
|
+
.number()
|
|
259
|
+
.min(0.05)
|
|
260
|
+
.max(1)
|
|
261
|
+
.optional()
|
|
262
|
+
.describe("QR opacity from 0.05 to 1 (fresh TikTok 0.37, X 0.5)"),
|
|
263
|
+
handle_opacity: z
|
|
264
|
+
.number()
|
|
265
|
+
.min(0.05)
|
|
266
|
+
.max(1)
|
|
267
|
+
.optional()
|
|
268
|
+
.describe("Handle opacity from 0.05 to 1 (fresh TikTok 0.28, X 0.5)"),
|
|
269
|
+
scale: z
|
|
270
|
+
.number()
|
|
271
|
+
.min(0.5)
|
|
272
|
+
.max(2)
|
|
273
|
+
.optional()
|
|
274
|
+
.describe("Size multiplier on the default badge size (default 1)"),
|
|
275
|
+
card_asset_id: z
|
|
276
|
+
.string()
|
|
277
|
+
.optional()
|
|
278
|
+
.describe("Reuse an existing image asset for the primary QR or handle watermark"),
|
|
279
|
+
mode: z
|
|
280
|
+
.enum(["burn", "overlay"])
|
|
281
|
+
.optional()
|
|
282
|
+
.describe("burn (default): composite into the video file and reprocess; overlay: store render-time OSG settings only"),
|
|
283
|
+
wait: z
|
|
284
|
+
.boolean()
|
|
285
|
+
.optional()
|
|
286
|
+
.describe("Burn mode: wait for the burn and reprocess to finish (default true). false returns status=accepted with the queue job_id as soon as the burn is queued"),
|
|
287
|
+
},
|
|
288
|
+
handler: async (args) => {
|
|
289
|
+
const { book_id, asset_id, handle, url, save_default, include_qr = true, include_handle = true, position: positionArg, handle_vertical_percent: handleVerticalArg, handle_position: handlePositionArg, platform: platformArg, include_platform_icon, include_handle_border: borderArg, handle_padding: paddingArg, handle_padding_y: paddingYArg, handle_rounding: roundingArg, qr_start_seconds, qr_end_seconds, handle_start_seconds, handle_end_seconds, fade_seconds: fadeArg, qr_opacity, handle_opacity, scale: scaleArg, card_asset_id, mode: modeArg, wait = true, } = args;
|
|
290
|
+
const platform = platformArg ?? "tiktok";
|
|
291
|
+
const config = await readWatermarkConfig(engine, platform);
|
|
292
|
+
const profile = effectiveWatermarkProfile(config, platform);
|
|
293
|
+
const position = positionArg ?? (profile.qrPosition === "auto" ? "auto" : profile.qrPosition);
|
|
294
|
+
const handle_vertical_percent = handleVerticalArg ?? profile.handleVerticalPercent;
|
|
295
|
+
const handle_position = handlePositionArg ?? profile.handlePlacement;
|
|
296
|
+
const include_handle_border = borderArg ?? profile.includeHandleBorder;
|
|
297
|
+
const handle_padding = paddingArg ?? profile.handlePaddingX;
|
|
298
|
+
const handle_padding_y = paddingYArg ?? profile.handlePaddingY;
|
|
299
|
+
const handle_rounding = roundingArg ?? profile.handleRounding;
|
|
300
|
+
const fade_seconds = fadeArg ?? profile.fadeSeconds;
|
|
301
|
+
const scale = scaleArg ?? profile.scale;
|
|
302
|
+
const mode = modeArg ?? profile.mode;
|
|
303
|
+
const includePlatformIcon = include_platform_icon ?? profile.includePlatformIcon;
|
|
304
|
+
let resolvedQrStartSeconds = qr_start_seconds;
|
|
305
|
+
let resolvedQrEndSeconds = qr_end_seconds;
|
|
306
|
+
let resolvedHandleStartSeconds = handle_start_seconds;
|
|
307
|
+
let resolvedHandleEndSeconds = handle_end_seconds;
|
|
308
|
+
const resolvedQrOpacity = qr_opacity ?? profile.qrOpacity;
|
|
309
|
+
const resolvedHandleOpacity = handle_opacity ?? profile.handleOpacity;
|
|
310
|
+
const callerSetTiming = qr_start_seconds !== undefined
|
|
311
|
+
|| qr_end_seconds !== undefined
|
|
312
|
+
|| handle_start_seconds !== undefined
|
|
313
|
+
|| handle_end_seconds !== undefined;
|
|
314
|
+
const storedQrStart = config.profile?.qrStartSeconds;
|
|
315
|
+
const storedQrEnd = config.profile?.qrEndSeconds;
|
|
316
|
+
const storedAbsolute = !callerSetTiming
|
|
317
|
+
&& (typeof storedQrStart === "number" || typeof storedQrEnd === "number");
|
|
318
|
+
if (!include_qr && !include_handle) {
|
|
319
|
+
return {
|
|
320
|
+
isError: true,
|
|
321
|
+
content: [{ type: "text", text: "include_qr or include_handle must be true" }],
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
const timingError = (include_handle
|
|
325
|
+
? watermarkTimingError("handle", resolvedHandleStartSeconds, resolvedHandleEndSeconds)
|
|
326
|
+
: null) ?? (include_qr
|
|
327
|
+
? watermarkTimingError("qr", resolvedQrStartSeconds, resolvedQrEndSeconds)
|
|
328
|
+
: null);
|
|
329
|
+
if (timingError) {
|
|
330
|
+
return {
|
|
331
|
+
isError: true,
|
|
332
|
+
content: [{ type: "text", text: timingError }],
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
const kindGate = await requireArtifactKind(engine, asset_id, ["video"], "apply_watermark");
|
|
336
|
+
if (kindGate)
|
|
337
|
+
return kindGate;
|
|
338
|
+
const manifestResult = await engine.files.manifest(asset_id);
|
|
339
|
+
if (!manifestResult.ok) {
|
|
340
|
+
return { isError: true, content: [{ type: "text", text: manifestResult.error.message }] };
|
|
341
|
+
}
|
|
342
|
+
const videoFile = findVideoFile(manifestResult.value.files);
|
|
343
|
+
if (!videoFile) {
|
|
344
|
+
return { isError: true, content: [{ type: "text", text: `No video file found in asset ${asset_id}` }] };
|
|
345
|
+
}
|
|
346
|
+
const assetDir = manifestResult.value.path;
|
|
347
|
+
const videoPath = join(assetDir, videoFile.name);
|
|
348
|
+
try {
|
|
349
|
+
const video = await probeVideo(videoPath);
|
|
350
|
+
const profileTimesTheClip = profile.handleEndSeconds != null || profile.qrLeadSeconds != null;
|
|
351
|
+
if (storedAbsolute) {
|
|
352
|
+
if (typeof storedQrStart === "number")
|
|
353
|
+
resolvedQrStartSeconds = storedQrStart;
|
|
354
|
+
if (typeof storedQrEnd === "number")
|
|
355
|
+
resolvedQrEndSeconds = storedQrEnd;
|
|
356
|
+
if (typeof config.profile?.handleStartSeconds === "number")
|
|
357
|
+
resolvedHandleStartSeconds = config.profile.handleStartSeconds;
|
|
358
|
+
if (typeof config.profile?.handleEndSeconds === "number")
|
|
359
|
+
resolvedHandleEndSeconds = config.profile.handleEndSeconds;
|
|
360
|
+
}
|
|
361
|
+
else if (profileTimesTheClip
|
|
362
|
+
&& resolvedQrStartSeconds === undefined
|
|
363
|
+
&& resolvedQrEndSeconds === undefined
|
|
364
|
+
&& resolvedHandleStartSeconds === undefined
|
|
365
|
+
&& resolvedHandleEndSeconds === undefined) {
|
|
366
|
+
const windows = watermarkWindowsFor(profile, video.videoDuration);
|
|
367
|
+
resolvedQrStartSeconds = windows.qrStartSeconds;
|
|
368
|
+
resolvedQrEndSeconds = windows.qrEndSeconds;
|
|
369
|
+
resolvedHandleStartSeconds = windows.handleStartSeconds;
|
|
370
|
+
resolvedHandleEndSeconds = windows.handleEndSeconds;
|
|
371
|
+
}
|
|
372
|
+
if (include_qr)
|
|
373
|
+
effectiveWatermarkTiming("QR watermark", video.videoDuration, resolvedQrStartSeconds, resolvedQrEndSeconds);
|
|
374
|
+
if (include_handle)
|
|
375
|
+
effectiveWatermarkTiming("Handle watermark", video.videoDuration, resolvedHandleStartSeconds, resolvedHandleEndSeconds);
|
|
376
|
+
if (mode === "overlay" && include_qr && include_handle && ((resolvedQrStartSeconds ?? 0) !== (resolvedHandleStartSeconds ?? 0)
|
|
377
|
+
|| Math.min(resolvedQrEndSeconds ?? video.videoDuration, video.videoDuration)
|
|
378
|
+
!== Math.min(resolvedHandleEndSeconds ?? video.videoDuration, video.videoDuration)
|
|
379
|
+
|| resolvedQrOpacity !== resolvedHandleOpacity)) {
|
|
380
|
+
throw new Error("Independent QR and handle timing or opacity requires mode=burn");
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return { isError: true, content: [{ type: "text", text: error instanceof Error ? error.message : String(error) }] };
|
|
385
|
+
}
|
|
386
|
+
const fallbackHandle = config.savedHandle ?? config.authenticatedHandle ?? config.recent[0]
|
|
387
|
+
?? profile.handle;
|
|
388
|
+
const resolvedHandle = normalizeHandle(handle ?? (include_handle || url === undefined ? fallbackHandle : "")) ?? "";
|
|
389
|
+
if (include_handle && !resolvedHandle) {
|
|
390
|
+
return {
|
|
391
|
+
isError: true,
|
|
392
|
+
content: [{ type: "text", text: "handle must be a non-empty string" }],
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
const storedProfileUrl = typeof config.profile?.profileUrl === "string" ? config.profile.profileUrl.trim() : "";
|
|
396
|
+
const resolvedUrl = (url ?? (storedProfileUrl || (resolvedHandle ? defaultUrl(resolvedHandle, platform) : ""))).trim();
|
|
397
|
+
if (include_qr && !resolvedUrl) {
|
|
398
|
+
return {
|
|
399
|
+
isError: true,
|
|
400
|
+
content: [{ type: "text", text: "url or handle must provide a QR destination" }],
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
const resolvedHandlePosition = include_qr
|
|
404
|
+
? handle_position
|
|
405
|
+
: "center";
|
|
406
|
+
const appearance = {
|
|
407
|
+
platform,
|
|
408
|
+
includePlatformIcon,
|
|
409
|
+
includeHandleBorder: include_handle_border,
|
|
410
|
+
handlePadding: handle_padding,
|
|
411
|
+
handlePaddingY: handle_padding_y,
|
|
412
|
+
handleRounding: handle_rounding,
|
|
413
|
+
};
|
|
414
|
+
const savedProfile = watermarkProfileToSave(profile, {
|
|
415
|
+
handle: resolvedHandle,
|
|
416
|
+
profileUrl: resolvedUrl,
|
|
417
|
+
includePlatformIcon,
|
|
418
|
+
includeHandleBorder: include_handle_border,
|
|
419
|
+
handlePaddingX: handle_padding,
|
|
420
|
+
handlePaddingY: handle_padding_y,
|
|
421
|
+
handleRounding: handle_rounding,
|
|
422
|
+
scale,
|
|
423
|
+
mode,
|
|
424
|
+
handlePlacement: handle_position,
|
|
425
|
+
handleVerticalPercent: handle_vertical_percent,
|
|
426
|
+
handleOpacity: resolvedHandleOpacity,
|
|
427
|
+
qrOpacity: resolvedQrOpacity,
|
|
428
|
+
fadeSeconds: fade_seconds,
|
|
429
|
+
}, {
|
|
430
|
+
explicit: callerSetTiming,
|
|
431
|
+
qrStartSeconds: resolvedQrStartSeconds,
|
|
432
|
+
qrEndSeconds: resolvedQrEndSeconds,
|
|
433
|
+
handleStartSeconds: resolvedHandleStartSeconds,
|
|
434
|
+
handleEndSeconds: resolvedHandleEndSeconds,
|
|
435
|
+
});
|
|
436
|
+
let cardAssetId;
|
|
437
|
+
let qrAssetId = null;
|
|
438
|
+
let handleAssetId = null;
|
|
439
|
+
const variant = watermarkVariantForApply(mode, include_qr, include_handle);
|
|
440
|
+
if (card_asset_id) {
|
|
441
|
+
const cardGate = await requireArtifactKind(engine, card_asset_id, ["image"], "card_asset_id");
|
|
442
|
+
if (cardGate)
|
|
443
|
+
return cardGate;
|
|
444
|
+
cardAssetId = card_asset_id;
|
|
445
|
+
if (mode === "burn" && include_qr)
|
|
446
|
+
qrAssetId = card_asset_id;
|
|
447
|
+
if (mode === "burn" && include_handle && !include_qr)
|
|
448
|
+
handleAssetId = card_asset_id;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
const resolved = await resolveWatermarkAsset(engine, resolvedHandle, resolvedUrl, variant, appearance);
|
|
452
|
+
if (!resolved.ok) {
|
|
453
|
+
return {
|
|
454
|
+
isError: true,
|
|
455
|
+
content: [{ type: "text", text: resolved.error }],
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
cardAssetId = resolved.assetId;
|
|
459
|
+
if (mode === "burn" && include_qr)
|
|
460
|
+
qrAssetId = resolved.assetId;
|
|
461
|
+
if (mode === "burn" && include_handle && !include_qr)
|
|
462
|
+
handleAssetId = resolved.assetId;
|
|
463
|
+
}
|
|
464
|
+
if (mode === "burn" && include_handle && handleAssetId === null) {
|
|
465
|
+
const resolved = await resolveWatermarkAsset(engine, resolvedHandle, resolvedUrl, "handle", appearance);
|
|
466
|
+
if (!resolved.ok) {
|
|
467
|
+
return {
|
|
468
|
+
isError: true,
|
|
469
|
+
content: [{ type: "text", text: resolved.error }],
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
handleAssetId = resolved.assetId;
|
|
473
|
+
}
|
|
474
|
+
const badgeAspect = variant === "qr"
|
|
475
|
+
? QR_ONLY_BADGE_ASPECT
|
|
476
|
+
: watermarkBadgeAspect(include_qr, handle_padding_y);
|
|
477
|
+
const scaling = { aspect: badgeAspect, userScale: scale };
|
|
478
|
+
const summary = {
|
|
479
|
+
asset_id,
|
|
480
|
+
card_asset_id: cardAssetId,
|
|
481
|
+
handle: resolvedHandle || null,
|
|
482
|
+
url: resolvedUrl,
|
|
483
|
+
include_qr,
|
|
484
|
+
include_handle,
|
|
485
|
+
qr_opacity: resolvedQrOpacity,
|
|
486
|
+
handle_opacity: resolvedHandleOpacity,
|
|
487
|
+
scale,
|
|
488
|
+
requested_position: position,
|
|
489
|
+
handle_vertical_percent,
|
|
490
|
+
handle_position: include_handle ? resolvedHandlePosition : null,
|
|
491
|
+
platform,
|
|
492
|
+
include_platform_icon: includePlatformIcon,
|
|
493
|
+
include_handle_border,
|
|
494
|
+
handle_padding,
|
|
495
|
+
handle_padding_y,
|
|
496
|
+
handle_rounding,
|
|
497
|
+
fade_seconds,
|
|
498
|
+
qr_start_seconds: include_qr ? resolvedQrStartSeconds ?? null : null,
|
|
499
|
+
qr_end_seconds: include_qr ? resolvedQrEndSeconds ?? null : null,
|
|
500
|
+
handle_start_seconds: include_handle ? resolvedHandleStartSeconds ?? null : null,
|
|
501
|
+
handle_end_seconds: include_handle ? resolvedHandleEndSeconds ?? null : null,
|
|
502
|
+
};
|
|
503
|
+
if (mode === "overlay") {
|
|
504
|
+
const overlayOpacity = include_qr ? resolvedQrOpacity : resolvedHandleOpacity;
|
|
505
|
+
const overlayStartSeconds = include_qr ? resolvedQrStartSeconds : resolvedHandleStartSeconds;
|
|
506
|
+
const overlayEndSeconds = include_qr ? resolvedQrEndSeconds : resolvedHandleEndSeconds;
|
|
507
|
+
const placement = await resolveOverlayPlacement(engine, book_id, asset_id, assetDir, videoFile, position, scaling);
|
|
508
|
+
const written = await writeOsgSettings(engine, asset_id, (orientation) => ({
|
|
509
|
+
osgImagePath: null,
|
|
510
|
+
osgAssetId: cardAssetId,
|
|
511
|
+
osgPosition: placement.positions[orientation],
|
|
512
|
+
osgSize: WATERMARK_OSG_SIZE,
|
|
513
|
+
osgScale: watermarkOsgScale(orientation, scale),
|
|
514
|
+
osgOpacity: overlayOpacity,
|
|
515
|
+
osgStartSeconds: overlayStartSeconds ?? null,
|
|
516
|
+
osgEndSeconds: overlayEndSeconds ?? null,
|
|
517
|
+
osgTailSeconds: null,
|
|
518
|
+
}));
|
|
519
|
+
if (!written.ok) {
|
|
520
|
+
return {
|
|
521
|
+
isError: true,
|
|
522
|
+
content: [{ type: "text", text: written.error }],
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
await recordHandle(resolvedHandle, { saveDefault: save_default, platform }, engine).catch(() => undefined);
|
|
526
|
+
if (save_default && resolvedHandle) {
|
|
527
|
+
await saveWatermarkProfile(savedProfile, platform, engine).catch(() => undefined);
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
content: [{
|
|
531
|
+
type: "text",
|
|
532
|
+
text: JSON.stringify({
|
|
533
|
+
...summary,
|
|
534
|
+
...(placement.jobId !== undefined ? { job_id: placement.jobId } : {}),
|
|
535
|
+
mode: "overlay",
|
|
536
|
+
opacity: overlayOpacity,
|
|
537
|
+
positions: placement.positions,
|
|
538
|
+
placement_source: placement.placementSource,
|
|
539
|
+
}),
|
|
540
|
+
}],
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
// Burn mode: composite the badge into the source video so the change is
|
|
544
|
+
// visible in the notebook cell (renders and proxies both inherit it).
|
|
545
|
+
const badge = await resolveOsgImagePath(engine, cardAssetId);
|
|
546
|
+
if (!badge.ok) {
|
|
547
|
+
return {
|
|
548
|
+
isError: true,
|
|
549
|
+
content: [{ type: "text", text: badge.error }],
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
let handlePath;
|
|
553
|
+
if (include_handle) {
|
|
554
|
+
if (handleAssetId === null) {
|
|
555
|
+
return {
|
|
556
|
+
isError: true,
|
|
557
|
+
content: [{ type: "text", text: "Handle watermark asset is unavailable" }],
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
const handleBadge = handleAssetId === cardAssetId
|
|
561
|
+
? badge
|
|
562
|
+
: await resolveOsgImagePath(engine, handleAssetId);
|
|
563
|
+
if (!handleBadge.ok) {
|
|
564
|
+
return {
|
|
565
|
+
isError: true,
|
|
566
|
+
content: [{ type: "text", text: handleBadge.error }],
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
handlePath = handleBadge.path;
|
|
570
|
+
}
|
|
571
|
+
// A handle-only burn has nothing to place; the corner is inert.
|
|
572
|
+
const placement = include_qr
|
|
573
|
+
? await resolveBurnPlacement(engine, asset_id, videoFile, videoPath, position, scaling)
|
|
574
|
+
: { corner: DEFAULT_POSITION, placementSource: "fixed", visionRequest: null };
|
|
575
|
+
const burnJobId = `burn-watermark-${asset_id}-${Date.now()}`;
|
|
576
|
+
const payload = {
|
|
577
|
+
type: "burn_watermark",
|
|
578
|
+
assetId: asset_id,
|
|
579
|
+
assetDir,
|
|
580
|
+
bookId: book_id,
|
|
581
|
+
videoPath,
|
|
582
|
+
...(include_qr ? { qrPath: badge.path } : {}),
|
|
583
|
+
...(handlePath !== undefined ? { handlePath } : {}),
|
|
584
|
+
corner: placement.corner,
|
|
585
|
+
placementSource: placement.placementSource,
|
|
586
|
+
...(placement.visionRequest ? { placement: placement.visionRequest } : {}),
|
|
587
|
+
qrOpacity: resolvedQrOpacity,
|
|
588
|
+
handleOpacity: resolvedHandleOpacity,
|
|
589
|
+
userScale: scale,
|
|
590
|
+
fadeSeconds: fade_seconds,
|
|
591
|
+
...(include_handle ? { handleVerticalPercent: handle_vertical_percent } : {}),
|
|
592
|
+
...(include_handle ? { handlePlacement: resolvedHandlePosition } : {}),
|
|
593
|
+
...(include_handle ? { handlePaddingY: handle_padding_y } : {}),
|
|
594
|
+
...(resolvedQrStartSeconds !== undefined
|
|
595
|
+
? { qrStartSeconds: resolvedQrStartSeconds }
|
|
596
|
+
: {}),
|
|
597
|
+
...(resolvedQrEndSeconds !== undefined
|
|
598
|
+
? { qrEndSeconds: resolvedQrEndSeconds }
|
|
599
|
+
: {}),
|
|
600
|
+
...(include_handle && resolvedHandleStartSeconds !== undefined
|
|
601
|
+
? { handleStartSeconds: resolvedHandleStartSeconds }
|
|
602
|
+
: {}),
|
|
603
|
+
...(include_handle && resolvedHandleEndSeconds !== undefined
|
|
604
|
+
? { handleEndSeconds: resolvedHandleEndSeconds }
|
|
605
|
+
: {}),
|
|
606
|
+
};
|
|
607
|
+
const burnSummary = {
|
|
608
|
+
...summary,
|
|
609
|
+
mode: "burn",
|
|
610
|
+
operation_id: burnJobId,
|
|
611
|
+
qr_asset_id: qrAssetId,
|
|
612
|
+
handle_asset_id: handleAssetId,
|
|
613
|
+
handle_position: include_handle ? resolvedHandlePosition : null,
|
|
614
|
+
};
|
|
615
|
+
await engine.jobs.failures.clear(asset_id);
|
|
616
|
+
await recordHandle(resolvedHandle, { saveDefault: save_default, platform }, engine).catch(() => undefined);
|
|
617
|
+
if (save_default && resolvedHandle) {
|
|
618
|
+
await saveWatermarkProfile(savedProfile, platform, engine).catch(() => undefined);
|
|
619
|
+
}
|
|
620
|
+
if (!wait) {
|
|
621
|
+
// Return as soon as the burn is queued; the frame-aware corner is
|
|
622
|
+
// settled inside the job, so it stays pending here.
|
|
623
|
+
const job = await queue.enqueue(burnJobId, payload);
|
|
624
|
+
const pending = placement.visionRequest !== null;
|
|
625
|
+
return {
|
|
626
|
+
content: [{
|
|
627
|
+
type: "text",
|
|
628
|
+
text: JSON.stringify({
|
|
629
|
+
...burnSummary,
|
|
630
|
+
status: "accepted",
|
|
631
|
+
job_id: job.id,
|
|
632
|
+
position: include_qr && !pending ? placement.corner : null,
|
|
633
|
+
placement_source: pending ? "pending" : placement.placementSource,
|
|
634
|
+
}),
|
|
635
|
+
}],
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
let burned;
|
|
639
|
+
try {
|
|
640
|
+
burned = await queue.enqueueAndWait(burnJobId, payload, BURN_TIMEOUT_MS);
|
|
641
|
+
const readiness = await waitForAssetReadyDef(engine).handler({
|
|
642
|
+
asset_id,
|
|
643
|
+
book_id,
|
|
644
|
+
timeout_seconds: 300,
|
|
645
|
+
});
|
|
646
|
+
if (readiness.isError) {
|
|
647
|
+
const text = readiness.content.find((entry) => entry.type === "text")?.text;
|
|
648
|
+
throw new Error(text ?? "Post-watermark processing failed");
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
catch (err) {
|
|
652
|
+
return {
|
|
653
|
+
isError: true,
|
|
654
|
+
content: [{ type: "text", text: `Watermark failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
content: [{
|
|
659
|
+
type: "text",
|
|
660
|
+
text: JSON.stringify({
|
|
661
|
+
...burnSummary,
|
|
662
|
+
status: "completed",
|
|
663
|
+
position: include_qr ? burned.corner : null,
|
|
664
|
+
placement_source: burned.placementSource,
|
|
665
|
+
}),
|
|
666
|
+
}],
|
|
667
|
+
};
|
|
668
|
+
},
|
|
669
|
+
};
|
|
670
|
+
}
|