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,3335 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
3
|
+
import { LIGHTNING_VIDEO_MODEL, payloadPrompt, submittedGenerationPrompt, } from "../lightning-defaults.js";
|
|
4
|
+
import { v5 as uuidv5, v7 as uuidv7 } from "uuid";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { MVP_CONTRACT_VERSION, resolveNotebookCellArtifactId, } from "videobook-engine";
|
|
7
|
+
import { notebookGridTag } from "videobook-engine/notebook-grid";
|
|
8
|
+
import { logger } from "../logger.js";
|
|
9
|
+
import { mutateNotebook } from "../notebook-mutations.js";
|
|
10
|
+
import { removeCreditFailedOutput } from "../notebook-credit-retry.js";
|
|
11
|
+
import { allocateGenerateAudioResult, allocateGenerateImageResult, allocateGenerateVideoResult, ensureGenerationMediaOutputs, ensureGenerationMediaOutput, findGenerationMediaOutput, findGenerationResultForRun, isConcurrentGenerationProducer, isImageVideoGenerationResult, migrateStackedGenerationResults, normalizeLegacyExtractCells, } from "../notebook-cell-outputs.js";
|
|
12
|
+
import { queue, registerJobHandler, waitForJob } from "../queue.js";
|
|
13
|
+
import { smoothSceneConcatDef } from "../tools/smooth-scene-concat.js";
|
|
14
|
+
import { spliceVideoDef } from "../tools/splice-video.js";
|
|
15
|
+
import { tryResolveModel } from "../media/descriptor.js";
|
|
16
|
+
import { previewGeneration } from "../media/preview.js";
|
|
17
|
+
import { checkInsertVideoTiming, normalizeRefs } from "../media/refs.js";
|
|
18
|
+
import { REF_ROLES } from "../media/types.js";
|
|
19
|
+
import { captureFrame, concatSourceRanges, exportSequenceMaster, extractSourceAudio, MINIMUM_SPLIT_PIECE_SECONDS, normalizeSceneVideo, splitSourceRange, splitSourceRanges, } from "../notebook-media-ops.js";
|
|
20
|
+
import { analyzeNotebookSource } from "../notebook-live-analysis.js";
|
|
21
|
+
import { analysisFixtureForCell } from "../notebook-fixtures.js";
|
|
22
|
+
import { applyAnalysisResults } from "../notebook-workflow.js";
|
|
23
|
+
import { normalizeLegacyAnalyzeCell, normalizeLegacyAnalyzeDocument, } from "../notebook-action-catalog.js";
|
|
24
|
+
import { ANALYSIS_REASONING_EFFORT, INTELLIGENCE_MODEL, OPENAI_ANALYSIS_TOOL, } from "../openai-intelligence.js";
|
|
25
|
+
import { notebookGenerationInputContract, notebookGenerationInputError, notebookGenerationInputsAreCompatible, } from "../notebook-generation-contract.js";
|
|
26
|
+
import { notebookProviderFailureMessage } from "../notebook-provider-failure.js";
|
|
27
|
+
import { resolveGenerationInputReferences, } from "../notebook-reference-resolver.js";
|
|
28
|
+
import { resolvedInitialGenerationPrompt } from "../notebook-initial-generation-prompt.js";
|
|
29
|
+
import { sortByKindSlot } from "../generation-input-mentions.js";
|
|
30
|
+
import { generationPromptSurface } from "../prompt-surfaces.js";
|
|
31
|
+
import { recoverNotebookProviderTask } from "../provider-task-record.js";
|
|
32
|
+
import { prepareNotebookImageEdit } from "../notebook-image-edit.js";
|
|
33
|
+
function generationPlanStage(plan, cellId) {
|
|
34
|
+
const stages = Array.isArray(plan.stages) ? plan.stages : [];
|
|
35
|
+
const stage = stages.find((candidate) => {
|
|
36
|
+
if (!candidate
|
|
37
|
+
|| typeof candidate !== "object"
|
|
38
|
+
|| Array.isArray(candidate)
|
|
39
|
+
|| candidate.kind !== "queued") {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
if (!cellId)
|
|
43
|
+
return true;
|
|
44
|
+
const input = candidate.input;
|
|
45
|
+
return input
|
|
46
|
+
&& typeof input === "object"
|
|
47
|
+
&& !Array.isArray(input)
|
|
48
|
+
&& input.cell_id === cellId;
|
|
49
|
+
}) ?? stages.find((candidate) => candidate
|
|
50
|
+
&& typeof candidate === "object"
|
|
51
|
+
&& !Array.isArray(candidate)
|
|
52
|
+
&& candidate.kind === "queued");
|
|
53
|
+
return stage && typeof stage === "object" && !Array.isArray(stage)
|
|
54
|
+
? stage
|
|
55
|
+
: null;
|
|
56
|
+
}
|
|
57
|
+
export async function updateGenerationPlanState(engine, notebookId, planId, status, details = {}) {
|
|
58
|
+
const updatedAt = new Date().toISOString();
|
|
59
|
+
const result = await mutateNotebook(engine, notebookId, "update_generation_plan", (notebook) => {
|
|
60
|
+
const plans = notebook.generationPlans ?? [];
|
|
61
|
+
return {
|
|
62
|
+
...notebook,
|
|
63
|
+
generationPlans: plans.map((record) => record.planId === planId
|
|
64
|
+
&& (details.runId === undefined
|
|
65
|
+
|| generationPlanClaimRunId(record.plan) === details.runId)
|
|
66
|
+
? {
|
|
67
|
+
...record,
|
|
68
|
+
status,
|
|
69
|
+
updatedAt,
|
|
70
|
+
...(details.outputArtifactId
|
|
71
|
+
? { outputArtifactId: details.outputArtifactId }
|
|
72
|
+
: {}),
|
|
73
|
+
...(details.error ? { error: details.error } : {}),
|
|
74
|
+
}
|
|
75
|
+
: record),
|
|
76
|
+
};
|
|
77
|
+
}, {
|
|
78
|
+
planId,
|
|
79
|
+
status,
|
|
80
|
+
outputArtifactId: details.outputArtifactId,
|
|
81
|
+
error: details.error,
|
|
82
|
+
runId: details.runId,
|
|
83
|
+
});
|
|
84
|
+
if (!result.ok)
|
|
85
|
+
throw new Error(result.error.message);
|
|
86
|
+
}
|
|
87
|
+
export async function writeGenerationPlanProvenance(engine, payload, cell, outputArtifactId) {
|
|
88
|
+
const plan = payload.generationPlan;
|
|
89
|
+
if (!plan || typeof plan.planId !== "string")
|
|
90
|
+
return;
|
|
91
|
+
const stage = generationPlanStage(plan, cell.id);
|
|
92
|
+
const written = await engine.metadata.artifacts.write(outputArtifactId, "command-plan", {
|
|
93
|
+
planId: plan.planId,
|
|
94
|
+
parentActionId: typeof plan.parentActionId === "string"
|
|
95
|
+
? plan.parentActionId
|
|
96
|
+
: null,
|
|
97
|
+
notebookId: payload.notebookId,
|
|
98
|
+
cellId: cell.id,
|
|
99
|
+
actor: payload.actor ?? "videobook-user",
|
|
100
|
+
sourceSurface: plan.sourceSurface,
|
|
101
|
+
provider: stage?.provider,
|
|
102
|
+
model: stage?.model,
|
|
103
|
+
tool: stage?.tool,
|
|
104
|
+
input: stage?.input,
|
|
105
|
+
estimatedCostUsd: stage?.estimatedCostUsd,
|
|
106
|
+
estimateUnavailable: stage?.estimateUnavailable,
|
|
107
|
+
jobId: payload.jobId,
|
|
108
|
+
outputArtifactId,
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
});
|
|
111
|
+
if (!written.ok)
|
|
112
|
+
throw new Error(written.error.message);
|
|
113
|
+
}
|
|
114
|
+
export function notebookCellMediaType(cell) {
|
|
115
|
+
if (cell.type === "image"
|
|
116
|
+
|| cell.type === "generate_image")
|
|
117
|
+
return "image";
|
|
118
|
+
if (cell.type === "audio"
|
|
119
|
+
|| cell.type === "extract_audio"
|
|
120
|
+
|| cell.type === "generate_audio")
|
|
121
|
+
return "audio";
|
|
122
|
+
if (cell.type === "video"
|
|
123
|
+
|| cell.type === "split_video"
|
|
124
|
+
|| cell.type === "generate_video"
|
|
125
|
+
|| cell.type === "concat"
|
|
126
|
+
|| cell.type === "splice")
|
|
127
|
+
return "video";
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
export function notebookCellIsPaid(cell) {
|
|
131
|
+
return cell.operation === "rewrite_prompt"
|
|
132
|
+
|| (cell.tool === "edit_image_subjects" && cell.provider !== "local")
|
|
133
|
+
|| notebookCellModel(cell) !== null;
|
|
134
|
+
}
|
|
135
|
+
export function notebookCellIsRunnable(cell) {
|
|
136
|
+
if (cell.inputs?.mediaRole === "extract-audio-output")
|
|
137
|
+
return true;
|
|
138
|
+
return (cell.type !== "timeline"
|
|
139
|
+
&& cell.type !== "audio"
|
|
140
|
+
&& cell.type !== "image"
|
|
141
|
+
&& cell.type !== "video"
|
|
142
|
+
&& cell.type !== "character"
|
|
143
|
+
&& cell.type !== "analysis"
|
|
144
|
+
&& Boolean(cell.operation ?? cell.tool ?? cell.model));
|
|
145
|
+
}
|
|
146
|
+
function textResult(result) {
|
|
147
|
+
const text = result.content.find((item) => item.type === "text")?.text;
|
|
148
|
+
if (!text)
|
|
149
|
+
return null;
|
|
150
|
+
try {
|
|
151
|
+
return JSON.parse(text);
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return text;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function artifactReference(value) {
|
|
158
|
+
if (typeof value === "object" && value !== null) {
|
|
159
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
160
|
+
if ((key.endsWith("asset_id") || key.endsWith("AssetId")) && typeof nested === "string") {
|
|
161
|
+
return nested;
|
|
162
|
+
}
|
|
163
|
+
const found = artifactReference(nested);
|
|
164
|
+
if (found)
|
|
165
|
+
return found;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
for (const nested of value) {
|
|
170
|
+
const found = artifactReference(nested);
|
|
171
|
+
if (found)
|
|
172
|
+
return found;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
async function executeSpliceCell(engine, bookId, document, cell) {
|
|
178
|
+
const inputs = upstreamValues(document, cell);
|
|
179
|
+
const first = inputs.filter((entry) => entry.targetInput === "first");
|
|
180
|
+
const second = inputs.filter((entry) => entry.targetInput === "second");
|
|
181
|
+
if (inputs.length !== 2
|
|
182
|
+
|| first.length !== 1
|
|
183
|
+
|| second.length !== 1
|
|
184
|
+
|| !first[0].output
|
|
185
|
+
|| !second[0].output
|
|
186
|
+
|| notebookCellMediaType(first[0].cell) !== "video"
|
|
187
|
+
|| notebookCellMediaType(second[0].cell) !== "video") {
|
|
188
|
+
throw new Error(`Splice cell ${cell.id} requires exactly one completed video on first and second`);
|
|
189
|
+
}
|
|
190
|
+
const firstArtifact = engine.artifacts.get(first[0].output);
|
|
191
|
+
const secondArtifact = engine.artifacts.get(second[0].output);
|
|
192
|
+
if (!firstArtifact.ok)
|
|
193
|
+
throw new Error(firstArtifact.error.message);
|
|
194
|
+
if (!secondArtifact.ok)
|
|
195
|
+
throw new Error(secondArtifact.error.message);
|
|
196
|
+
const smooth = cell.operation === "smooth_scene_concat";
|
|
197
|
+
const overlap = Number(cell.inputs?.overlap ?? 0);
|
|
198
|
+
if (!smooth && (!Number.isFinite(overlap) || overlap < 0)) {
|
|
199
|
+
throw new Error("Splice overlap must be a non-negative number");
|
|
200
|
+
}
|
|
201
|
+
const transition = cell.inputs?.transitionSeconds;
|
|
202
|
+
if (smooth
|
|
203
|
+
&& transition !== undefined
|
|
204
|
+
&& (typeof transition !== "number" || !Number.isFinite(transition))) {
|
|
205
|
+
throw new Error("Splice transitionSeconds must be a finite number");
|
|
206
|
+
}
|
|
207
|
+
const definition = smooth
|
|
208
|
+
? smoothSceneConcatDef(engine)
|
|
209
|
+
: spliceVideoDef(engine);
|
|
210
|
+
const response = await definition.handler({
|
|
211
|
+
first_asset_id: firstArtifact.value.artifactId,
|
|
212
|
+
second_asset_id: secondArtifact.value.artifactId,
|
|
213
|
+
book_id: bookId,
|
|
214
|
+
...(smooth
|
|
215
|
+
? (transition === undefined ? {} : { transition_seconds: transition })
|
|
216
|
+
: { overlap }),
|
|
217
|
+
});
|
|
218
|
+
if (response.isError) {
|
|
219
|
+
throw new Error(response.content[0]?.text ?? `${cell.operation} failed`);
|
|
220
|
+
}
|
|
221
|
+
const result = textResult(response);
|
|
222
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
223
|
+
throw new Error(`${cell.operation} returned an invalid result`);
|
|
224
|
+
}
|
|
225
|
+
const record = result;
|
|
226
|
+
const jobId = typeof record.job_id === "string" ? record.job_id : null;
|
|
227
|
+
const output = typeof record.asset_id === "string" ? record.asset_id : null;
|
|
228
|
+
if (!jobId || !output) {
|
|
229
|
+
throw new Error(`${cell.operation} did not return a queued output`);
|
|
230
|
+
}
|
|
231
|
+
await waitForJob(jobId, 1_800_000);
|
|
232
|
+
const artifact = engine.artifacts.get(output);
|
|
233
|
+
if (!artifact.ok)
|
|
234
|
+
throw new Error(artifact.error.message);
|
|
235
|
+
return {
|
|
236
|
+
artifactId: artifact.value.artifactId,
|
|
237
|
+
tool: smooth ? "smooth_scene_concat" : "splice_video",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function executionState(document) {
|
|
241
|
+
return (document.execution ?? {});
|
|
242
|
+
}
|
|
243
|
+
function withExecution(document, cellId, state, outputArtifactId) {
|
|
244
|
+
const execution = { ...executionState(document), [cellId]: state };
|
|
245
|
+
return {
|
|
246
|
+
...document,
|
|
247
|
+
execution,
|
|
248
|
+
cells: document.cells.map((cell) => (cell.id === cellId
|
|
249
|
+
? {
|
|
250
|
+
...cell,
|
|
251
|
+
...(outputArtifactId ? { outputArtifactId } : {}),
|
|
252
|
+
inputs: {
|
|
253
|
+
...(cell.inputs ?? {}),
|
|
254
|
+
state: state.status,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
: cell)),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
export async function markNotebookCellQueued(engine, notebookId, cellId, runId) {
|
|
261
|
+
const notebook = engine.notebooks.read(notebookId);
|
|
262
|
+
if (!notebook.ok)
|
|
263
|
+
throw new Error(notebook.error.message);
|
|
264
|
+
const cell = notebook.value.cells.find((candidate) => candidate.id === cellId);
|
|
265
|
+
if (!cell)
|
|
266
|
+
throw new Error(`Notebook cell not found: ${cellId}`);
|
|
267
|
+
const updatedAt = new Date().toISOString();
|
|
268
|
+
const state = {
|
|
269
|
+
fingerprint: notebookCellFingerprint(notebook.value, cell),
|
|
270
|
+
status: "queued",
|
|
271
|
+
...(cell.outputArtifactId ? { outputArtifactId: cell.outputArtifactId } : {}),
|
|
272
|
+
runId,
|
|
273
|
+
startedAt: updatedAt,
|
|
274
|
+
updatedAt,
|
|
275
|
+
tool: cell.tool || cell.model || cell.operation,
|
|
276
|
+
};
|
|
277
|
+
const result = await mutateNotebook(engine, notebookId, "queue_notebook_cell", (current) => {
|
|
278
|
+
const existing = executionState(current)[cellId];
|
|
279
|
+
if (existing?.runId === runId && existing.status !== "queued")
|
|
280
|
+
return current;
|
|
281
|
+
return withExecution(current, cellId, state);
|
|
282
|
+
}, { cellId, runId });
|
|
283
|
+
if (!result.ok)
|
|
284
|
+
throw new Error(result.error.message);
|
|
285
|
+
}
|
|
286
|
+
function upstreamValues(document, cell) {
|
|
287
|
+
const cells = new Map(document.cells.map((item) => [item.id, item]));
|
|
288
|
+
return document.edges
|
|
289
|
+
.filter((edge) => edge.target === cell.id)
|
|
290
|
+
.flatMap((edge) => {
|
|
291
|
+
const source = cells.get(edge.source);
|
|
292
|
+
if (!source)
|
|
293
|
+
return [];
|
|
294
|
+
return [{
|
|
295
|
+
targetInput: edge.targetInput,
|
|
296
|
+
cell: source,
|
|
297
|
+
output: resolveNotebookCellArtifactId(document, source),
|
|
298
|
+
}];
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
function stableCellInputs(inputs) {
|
|
302
|
+
const volatile = new Set([
|
|
303
|
+
"state",
|
|
304
|
+
"resolvedPrompt",
|
|
305
|
+
"dispatchedPayload",
|
|
306
|
+
"rawRewriteResult",
|
|
307
|
+
"providerArtifactId",
|
|
308
|
+
"normalizedFromArtifactId",
|
|
309
|
+
"normalizedDurationSeconds",
|
|
310
|
+
"sequenceRevision",
|
|
311
|
+
"sequenceId",
|
|
312
|
+
"clipArtifactIds",
|
|
313
|
+
"audioArtifactId",
|
|
314
|
+
"assemblyDurationFrames",
|
|
315
|
+
"shotCount",
|
|
316
|
+
"outputVersionArtifactIds",
|
|
317
|
+
"atSeconds",
|
|
318
|
+
]);
|
|
319
|
+
return Object.fromEntries(Object.entries(inputs ?? {}).filter(([key]) => !volatile.has(key)));
|
|
320
|
+
}
|
|
321
|
+
export function notebookCellFingerprint(document, cell) {
|
|
322
|
+
const payload = {
|
|
323
|
+
type: cell.type,
|
|
324
|
+
id: cell.id,
|
|
325
|
+
prompt: cell.prompt ?? "",
|
|
326
|
+
provider: cell.provider ?? "",
|
|
327
|
+
model: cell.model ?? "",
|
|
328
|
+
operation: cell.operation ?? "",
|
|
329
|
+
tool: cell.tool ?? "",
|
|
330
|
+
inputs: stableCellInputs(cell.inputs),
|
|
331
|
+
upstream: upstreamValues(document, cell).map(({ targetInput, cell: source, output }) => ({
|
|
332
|
+
targetInput,
|
|
333
|
+
sourceId: source.id,
|
|
334
|
+
sourceType: source.type,
|
|
335
|
+
prompt: source.prompt ?? "",
|
|
336
|
+
provider: source.provider ?? "",
|
|
337
|
+
model: source.model ?? "",
|
|
338
|
+
operation: source.operation ?? "",
|
|
339
|
+
tool: source.tool ?? "",
|
|
340
|
+
output: output ?? null,
|
|
341
|
+
outputHash: typeof source.inputs?.objectHash === "string"
|
|
342
|
+
? source.inputs.objectHash
|
|
343
|
+
: null,
|
|
344
|
+
sequenceRevision: typeof source.inputs?.sequenceRevision === "string"
|
|
345
|
+
? source.inputs.sequenceRevision
|
|
346
|
+
: null,
|
|
347
|
+
range: {
|
|
348
|
+
startSeconds: source.inputs?.startSeconds ?? null,
|
|
349
|
+
endSeconds: source.inputs?.endSeconds ?? null,
|
|
350
|
+
},
|
|
351
|
+
})),
|
|
352
|
+
};
|
|
353
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
354
|
+
}
|
|
355
|
+
export function notebookCellDefinitionFingerprint(document, cell) {
|
|
356
|
+
const payload = {
|
|
357
|
+
type: cell.type,
|
|
358
|
+
id: cell.id,
|
|
359
|
+
prompt: cell.prompt ?? "",
|
|
360
|
+
provider: cell.provider ?? "",
|
|
361
|
+
model: cell.model ?? "",
|
|
362
|
+
operation: cell.operation ?? "",
|
|
363
|
+
tool: cell.tool ?? "",
|
|
364
|
+
inputs: stableCellInputs(cell.inputs),
|
|
365
|
+
upstream: upstreamValues(document, cell).map(({ targetInput, cell: source }) => ({
|
|
366
|
+
targetInput,
|
|
367
|
+
sourceId: source.id,
|
|
368
|
+
sourceType: source.type,
|
|
369
|
+
prompt: source.prompt ?? "",
|
|
370
|
+
provider: source.provider ?? "",
|
|
371
|
+
model: source.model ?? "",
|
|
372
|
+
operation: source.operation ?? "",
|
|
373
|
+
tool: source.tool ?? "",
|
|
374
|
+
inputs: stableCellInputs(source.inputs),
|
|
375
|
+
})),
|
|
376
|
+
};
|
|
377
|
+
return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
|
|
378
|
+
}
|
|
379
|
+
function firstUpstreamArtifact(document, cell, targetInput) {
|
|
380
|
+
const upstream = upstreamValues(document, cell)
|
|
381
|
+
.filter((entry) => (targetInput ? entry.targetInput === targetInput : true))
|
|
382
|
+
.map((entry) => entry.output)
|
|
383
|
+
.find((value) => typeof value === "string" && value.length > 0);
|
|
384
|
+
return upstream ?? null;
|
|
385
|
+
}
|
|
386
|
+
function requiredVideoInputArtifact(document, cell) {
|
|
387
|
+
const input = upstreamValues(document, cell)
|
|
388
|
+
.find((entry) => entry.targetInput === "video" || entry.targetInput === "segment");
|
|
389
|
+
if (!input) {
|
|
390
|
+
throw new Error(`${cell.type} cell ${cell.id} requires a video input`);
|
|
391
|
+
}
|
|
392
|
+
if (notebookCellMediaType(input.cell) !== "video"
|
|
393
|
+
|| input.cell.type === "split_video") {
|
|
394
|
+
throw new Error(`${cell.type} cell ${cell.id} input must be video`);
|
|
395
|
+
}
|
|
396
|
+
if (!input.output) {
|
|
397
|
+
throw new Error(`${cell.type} cell ${cell.id} video input is not completed`);
|
|
398
|
+
}
|
|
399
|
+
return input.output;
|
|
400
|
+
}
|
|
401
|
+
function requiredMediaOutputCellIds(document, cell, type) {
|
|
402
|
+
const connectedIds = new Set(document.edges
|
|
403
|
+
.filter((edge) => edge.source === cell.id
|
|
404
|
+
&& edge.targetInput === "media")
|
|
405
|
+
.map((edge) => edge.target));
|
|
406
|
+
const outputIds = new Set(document.cells
|
|
407
|
+
.filter((candidate) => connectedIds.has(candidate.id)
|
|
408
|
+
&& candidate.type === type)
|
|
409
|
+
.map((candidate) => candidate.id));
|
|
410
|
+
if (outputIds.size === 0) {
|
|
411
|
+
throw new Error(`${cell.type} cell ${cell.id} requires a downstream ${type} media cell`);
|
|
412
|
+
}
|
|
413
|
+
return outputIds;
|
|
414
|
+
}
|
|
415
|
+
function requiredTwoWaySplitVideoOutputCellIds(document, cell) {
|
|
416
|
+
const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
|
|
417
|
+
const first = new Set();
|
|
418
|
+
const remaining = new Set();
|
|
419
|
+
for (const candidate of document.cells) {
|
|
420
|
+
if (!videoCellIds.has(candidate.id))
|
|
421
|
+
continue;
|
|
422
|
+
if (candidate.inputs?.splitRole === "first")
|
|
423
|
+
first.add(candidate.id);
|
|
424
|
+
if (candidate.inputs?.splitRole === "remaining") {
|
|
425
|
+
remaining.add(candidate.id);
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
if (videoCellIds.size !== 2
|
|
429
|
+
|| first.size !== 1
|
|
430
|
+
|| remaining.size !== 1) {
|
|
431
|
+
throw new Error(`split_video cell ${cell.id} requires first and remaining video outputs`);
|
|
432
|
+
}
|
|
433
|
+
return { first, remaining };
|
|
434
|
+
}
|
|
435
|
+
function parseSplitOutputGroups(value) {
|
|
436
|
+
if (!Array.isArray(value) || value.length === 0) {
|
|
437
|
+
throw new Error("outputsSeconds must be a non-empty list of range groups");
|
|
438
|
+
}
|
|
439
|
+
return value.map((group) => {
|
|
440
|
+
if (!Array.isArray(group) || group.length === 0) {
|
|
441
|
+
throw new Error("Each split output group must contain at least one range");
|
|
442
|
+
}
|
|
443
|
+
return group.map((entry) => {
|
|
444
|
+
const record = entry && typeof entry === "object"
|
|
445
|
+
? entry
|
|
446
|
+
: {};
|
|
447
|
+
const startSeconds = Number(record.startSeconds);
|
|
448
|
+
const endSeconds = Number(record.endSeconds);
|
|
449
|
+
if (!Number.isFinite(startSeconds)
|
|
450
|
+
|| !Number.isFinite(endSeconds)
|
|
451
|
+
|| !(endSeconds > startSeconds)) {
|
|
452
|
+
throw new Error("Split output ranges must use finite start and end seconds");
|
|
453
|
+
}
|
|
454
|
+
return { startSeconds, endSeconds };
|
|
455
|
+
});
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
function requiredIndexedSplitVideoOutputCellIds(document, cell, chunkCount) {
|
|
459
|
+
const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
|
|
460
|
+
const outputIds = new Array(chunkCount);
|
|
461
|
+
for (const candidate of document.cells) {
|
|
462
|
+
if (!videoCellIds.has(candidate.id))
|
|
463
|
+
continue;
|
|
464
|
+
const chunkIndex = candidate.inputs?.chunkIndex;
|
|
465
|
+
const candidateChunkCount = candidate.inputs?.chunkCount;
|
|
466
|
+
if (!Number.isSafeInteger(chunkIndex)
|
|
467
|
+
|| Number(candidateChunkCount) !== chunkCount
|
|
468
|
+
|| Number(chunkIndex) < 0
|
|
469
|
+
|| Number(chunkIndex) >= chunkCount
|
|
470
|
+
|| outputIds[Number(chunkIndex)]) {
|
|
471
|
+
throw new Error(`split_video cell ${cell.id} has invalid indexed video outputs`);
|
|
472
|
+
}
|
|
473
|
+
outputIds[Number(chunkIndex)] = candidate.id;
|
|
474
|
+
}
|
|
475
|
+
if (videoCellIds.size !== chunkCount
|
|
476
|
+
|| outputIds.filter(Boolean).length !== chunkCount) {
|
|
477
|
+
throw new Error(`split_video cell ${cell.id} requires ${chunkCount} indexed video outputs`);
|
|
478
|
+
}
|
|
479
|
+
return Array.from({ length: chunkCount }, (_, index) => new Set([outputIds[index]]));
|
|
480
|
+
}
|
|
481
|
+
function splitFrameOutputCellIds(document, videoCellIds) {
|
|
482
|
+
const connectedIds = new Set(document.edges
|
|
483
|
+
.filter((edge) => videoCellIds.has(edge.source)
|
|
484
|
+
&& edge.targetInput === "segment")
|
|
485
|
+
.map((edge) => edge.target));
|
|
486
|
+
const first = new Set();
|
|
487
|
+
const last = new Set();
|
|
488
|
+
for (const candidate of document.cells) {
|
|
489
|
+
if (!connectedIds.has(candidate.id) || candidate.type !== "image")
|
|
490
|
+
continue;
|
|
491
|
+
if (candidate.inputs?.frameRole === "first")
|
|
492
|
+
first.add(candidate.id);
|
|
493
|
+
if (candidate.inputs?.frameRole === "last")
|
|
494
|
+
last.add(candidate.id);
|
|
495
|
+
}
|
|
496
|
+
return { first, last };
|
|
497
|
+
}
|
|
498
|
+
function withOutputVersionIds(cell, artifactId) {
|
|
499
|
+
const persisted = Array.isArray(cell.inputs?.outputVersionArtifactIds)
|
|
500
|
+
? cell.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string" && value.length > 0)
|
|
501
|
+
: [];
|
|
502
|
+
return [...new Set([
|
|
503
|
+
...persisted,
|
|
504
|
+
...(cell.outputArtifactId ? [cell.outputArtifactId] : []),
|
|
505
|
+
artifactId,
|
|
506
|
+
])];
|
|
507
|
+
}
|
|
508
|
+
function withMediaOutput(document, outputCellIds, artifactId, inputs) {
|
|
509
|
+
return {
|
|
510
|
+
...document,
|
|
511
|
+
cells: document.cells.map((candidate) => {
|
|
512
|
+
if (!outputCellIds.has(candidate.id))
|
|
513
|
+
return candidate;
|
|
514
|
+
if (isImageVideoGenerationResult(candidate)) {
|
|
515
|
+
const state = typeof inputs.state === "string"
|
|
516
|
+
? inputs.state
|
|
517
|
+
: "completed";
|
|
518
|
+
const nextInputs = {
|
|
519
|
+
...(candidate.inputs ?? {}),
|
|
520
|
+
...inputs,
|
|
521
|
+
state,
|
|
522
|
+
};
|
|
523
|
+
delete nextInputs.outputVersionArtifactIds;
|
|
524
|
+
return {
|
|
525
|
+
...candidate,
|
|
526
|
+
outputArtifactId: artifactId,
|
|
527
|
+
inputs: nextInputs,
|
|
528
|
+
};
|
|
529
|
+
}
|
|
530
|
+
const persisted = Array.isArray(candidate.inputs?.outputVersionArtifactIds)
|
|
531
|
+
? candidate.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string" && value.length > 0)
|
|
532
|
+
: [];
|
|
533
|
+
const outputVersionArtifactIds = [...new Set([
|
|
534
|
+
...persisted,
|
|
535
|
+
...(candidate.outputArtifactId ? [candidate.outputArtifactId] : []),
|
|
536
|
+
artifactId,
|
|
537
|
+
])];
|
|
538
|
+
return {
|
|
539
|
+
...candidate,
|
|
540
|
+
outputArtifactId: artifactId,
|
|
541
|
+
inputs: {
|
|
542
|
+
...(candidate.inputs ?? {}),
|
|
543
|
+
...inputs,
|
|
544
|
+
outputVersionArtifactIds,
|
|
545
|
+
state: "completed",
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}),
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
function rollbackGenerationMediaOutput(document, cell, staleArtifactId, outputCellIdHint) {
|
|
552
|
+
const mediaOutput = findGenerationResultForRun(document, cell, {
|
|
553
|
+
outputCellId: outputCellIdHint,
|
|
554
|
+
});
|
|
555
|
+
if (!mediaOutput)
|
|
556
|
+
return document;
|
|
557
|
+
const outputCellId = mediaOutput.outputCell.id;
|
|
558
|
+
const remainingExecution = { ...executionState(document) };
|
|
559
|
+
delete remainingExecution[outputCellId];
|
|
560
|
+
return {
|
|
561
|
+
...document,
|
|
562
|
+
cells: document.cells.map((candidate) => {
|
|
563
|
+
if (candidate.id !== outputCellId)
|
|
564
|
+
return candidate;
|
|
565
|
+
const versions = Array.isArray(candidate.inputs?.outputVersionArtifactIds)
|
|
566
|
+
? candidate.inputs.outputVersionArtifactIds.filter((value) => typeof value === "string"
|
|
567
|
+
&& value.length > 0
|
|
568
|
+
&& value !== staleArtifactId)
|
|
569
|
+
: [];
|
|
570
|
+
const restored = versions.at(-1);
|
|
571
|
+
const inputs = { ...(candidate.inputs ?? {}) };
|
|
572
|
+
delete inputs.state;
|
|
573
|
+
return {
|
|
574
|
+
...candidate,
|
|
575
|
+
outputArtifactId: restored,
|
|
576
|
+
inputs: {
|
|
577
|
+
...inputs,
|
|
578
|
+
outputVersionArtifactIds: versions,
|
|
579
|
+
...(restored ? { state: "completed" } : {}),
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
}),
|
|
583
|
+
execution: remainingExecution,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// A crash, an abort, or a failure predating the rollback in
|
|
587
|
+
// failAwaitingNotebookCell can leave the paired output cell holding an
|
|
588
|
+
// awaiting-output stamp and a placeholder artifact that will never land.
|
|
589
|
+
// Heal that residue when the producer starts a fresh run so the pair always
|
|
590
|
+
// begins clean and a later synchronous failure cannot expose stale state.
|
|
591
|
+
export function healGenerationMediaOutputResidue(document, cell, outputCellId) {
|
|
592
|
+
if (isConcurrentGenerationProducer(cell) && !outputCellId) {
|
|
593
|
+
return document;
|
|
594
|
+
}
|
|
595
|
+
const mediaOutput = findGenerationResultForRun(document, cell, { outputCellId });
|
|
596
|
+
if (!mediaOutput)
|
|
597
|
+
return document;
|
|
598
|
+
if (isConcurrentGenerationProducer(cell)
|
|
599
|
+
&& outputCellId
|
|
600
|
+
&& mediaOutput.outputCell.id !== outputCellId) {
|
|
601
|
+
return document;
|
|
602
|
+
}
|
|
603
|
+
const outputCell = mediaOutput.outputCell;
|
|
604
|
+
const awaitingInputs = outputCell.inputs?.state === "awaiting-output";
|
|
605
|
+
const awaitingExecution = executionState(document)[outputCell.id]?.status === "awaiting-output";
|
|
606
|
+
if (!awaitingInputs && !awaitingExecution)
|
|
607
|
+
return document;
|
|
608
|
+
return rollbackGenerationMediaOutput(document, cell, outputCell.outputArtifactId, outputCell.id);
|
|
609
|
+
}
|
|
610
|
+
function withSplitOutputs(document, targets, outputs) {
|
|
611
|
+
return {
|
|
612
|
+
...document,
|
|
613
|
+
cells: document.cells.map((candidate) => {
|
|
614
|
+
const outputArtifactId = targets.video.has(candidate.id)
|
|
615
|
+
? outputs.videoArtifactId
|
|
616
|
+
: targets.first.has(candidate.id)
|
|
617
|
+
? outputs.firstFrameArtifactId
|
|
618
|
+
: targets.last.has(candidate.id)
|
|
619
|
+
? outputs.lastFrameArtifactId
|
|
620
|
+
: null;
|
|
621
|
+
if (!outputArtifactId)
|
|
622
|
+
return candidate;
|
|
623
|
+
return {
|
|
624
|
+
...candidate,
|
|
625
|
+
outputArtifactId,
|
|
626
|
+
inputs: {
|
|
627
|
+
...(candidate.inputs ?? {}),
|
|
628
|
+
...(targets.video.has(candidate.id)
|
|
629
|
+
? { durationSeconds: outputs.durationSeconds }
|
|
630
|
+
: {
|
|
631
|
+
atSeconds: targets.first.has(candidate.id)
|
|
632
|
+
? outputs.firstAtSeconds
|
|
633
|
+
: outputs.lastAtSeconds,
|
|
634
|
+
}),
|
|
635
|
+
state: "completed",
|
|
636
|
+
},
|
|
637
|
+
};
|
|
638
|
+
}),
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
function promptFor(document, cell) {
|
|
642
|
+
const upstream = upstreamValues(document, cell)
|
|
643
|
+
.filter(({ cell: source }) => source.type === "prompt" || source.operation === "rewrite_prompt")
|
|
644
|
+
.map(({ cell: source }) => source.operation === "rewrite_prompt" && typeof source.inputs?.resolvedPrompt === "string"
|
|
645
|
+
? source.inputs.resolvedPrompt
|
|
646
|
+
: source.prompt ?? "")
|
|
647
|
+
.filter(Boolean);
|
|
648
|
+
const own = cell.operation === "rewrite_prompt"
|
|
649
|
+
? [
|
|
650
|
+
typeof cell.inputs?.featureBrief === "string"
|
|
651
|
+
? cell.inputs.featureBrief
|
|
652
|
+
: "",
|
|
653
|
+
cell.prompt ?? "",
|
|
654
|
+
cell.inputs?.shotContext
|
|
655
|
+
? `Shot context: ${JSON.stringify(cell.inputs.shotContext)}`
|
|
656
|
+
: "",
|
|
657
|
+
typeof cell.inputs?.audioContext === "string"
|
|
658
|
+
? cell.inputs.audioContext
|
|
659
|
+
: "",
|
|
660
|
+
]
|
|
661
|
+
: [
|
|
662
|
+
cell.type === "generate_image" || cell.type === "generate_video"
|
|
663
|
+
? !cell.prompt?.trim() && upstream.length > 0
|
|
664
|
+
? ""
|
|
665
|
+
: resolvedInitialGenerationPrompt(document, cell)
|
|
666
|
+
: cell.prompt ?? "",
|
|
667
|
+
];
|
|
668
|
+
return [...new Set([...own, ...upstream].filter(Boolean))]
|
|
669
|
+
.join("\n\n")
|
|
670
|
+
.trim();
|
|
671
|
+
}
|
|
672
|
+
function isImageLikeCell(source) {
|
|
673
|
+
return notebookCellMediaType(source) === "image";
|
|
674
|
+
}
|
|
675
|
+
function cellGenerationSettings(cell) {
|
|
676
|
+
const settings = cell.inputs?.generationSettings;
|
|
677
|
+
return settings && typeof settings === "object" && !Array.isArray(settings)
|
|
678
|
+
? settings
|
|
679
|
+
: {};
|
|
680
|
+
}
|
|
681
|
+
function outputsOf(entries) {
|
|
682
|
+
return entries.flatMap(({ output }) => typeof output === "string" ? [output] : []);
|
|
683
|
+
}
|
|
684
|
+
function upstreamOfKind(upstream, kind) {
|
|
685
|
+
return upstream.filter(({ cell: source, output }) => typeof output === "string" && notebookCellMediaType(source) === kind);
|
|
686
|
+
}
|
|
687
|
+
function toolArgsWithMentions(definition, document, cell, bookId, engine) {
|
|
688
|
+
const mentions = resolveGenerationInputReferences({ engine, document, cell, prompt: promptFor(document, cell) });
|
|
689
|
+
const args = {
|
|
690
|
+
...(cell.inputs ?? {}),
|
|
691
|
+
...cellGenerationSettings(cell),
|
|
692
|
+
book_id: bookId,
|
|
693
|
+
prompt: mentions.prompt,
|
|
694
|
+
};
|
|
695
|
+
delete args.generationMode;
|
|
696
|
+
delete args.generationSettings;
|
|
697
|
+
if (cell.operation === "rewrite_prompt" && "model" in definition.schema) {
|
|
698
|
+
args.model = typeof cell.inputs?.targetModel === "string" ? cell.inputs.targetModel : "seedance25";
|
|
699
|
+
}
|
|
700
|
+
const images = sortByKindSlot(upstreamOfKind(upstreamValues(document, cell), "image")
|
|
701
|
+
.filter(({ targetInput }) => targetInput !== "first_frame" && targetInput !== "last_frame"), "image");
|
|
702
|
+
if ("image_refs" in definition.schema && images.length > 0)
|
|
703
|
+
args.image_refs = outputsOf(images);
|
|
704
|
+
return { input: args, mentions };
|
|
705
|
+
}
|
|
706
|
+
function generationModeFor(model, document, cell, upstream) {
|
|
707
|
+
const contract = notebookGenerationInputContract(model);
|
|
708
|
+
const configured = typeof cell.inputs?.generationMode === "string" ? cell.inputs.generationMode : undefined;
|
|
709
|
+
if (contract.modes.some((mode) => mode.id === configured))
|
|
710
|
+
return configured;
|
|
711
|
+
const compatible = contract.modes.filter((mode) => notebookGenerationInputsAreCompatible(contract, document, cell, mode.id));
|
|
712
|
+
const hasFrameInput = upstream.some((entry) => entry.targetInput === "first_frame" || entry.targetInput === "last_frame");
|
|
713
|
+
return compatible.find((mode) => mode.id === (hasFrameInput ? "i2v" : "r2v"))?.id
|
|
714
|
+
?? compatible[0]?.id
|
|
715
|
+
?? configured;
|
|
716
|
+
}
|
|
717
|
+
function frameEntry(upstream, target, mode) {
|
|
718
|
+
return mode === "r2v"
|
|
719
|
+
? undefined
|
|
720
|
+
: upstream.find((entry) => entry.targetInput === target && typeof entry.output === "string" && isImageLikeCell(entry.cell));
|
|
721
|
+
}
|
|
722
|
+
function mediaRefsFor(model, upstream, mode) {
|
|
723
|
+
const roles = new Set(model.slots.map((slot) => slot.role));
|
|
724
|
+
const modeRoles = new Set(model.modes.find((candidate) => candidate.id === mode)?.roles ?? [...roles]);
|
|
725
|
+
let firstFrame = frameEntry(upstream, "first_frame", mode);
|
|
726
|
+
let lastFrame = frameEntry(upstream, "last_frame", mode);
|
|
727
|
+
const images = sortByKindSlot(upstreamOfKind(upstream, "image").filter(({ targetInput }) => mode === "r2v" || (targetInput !== "first_frame" && targetInput !== "last_frame")), "image");
|
|
728
|
+
const frameMode = mode === "i2v" || ((mode === undefined || mode === "default") && Boolean(firstFrame || lastFrame));
|
|
729
|
+
if (frameMode && !firstFrame && modeRoles.has("first_frame") && !modeRoles.has("image"))
|
|
730
|
+
firstFrame = images.shift();
|
|
731
|
+
if (frameMode && !lastFrame && modeRoles.has("last_frame") && !modeRoles.has("image"))
|
|
732
|
+
lastFrame = images.shift();
|
|
733
|
+
const videos = sortByKindSlot(upstreamOfKind(upstream, "video"), "video");
|
|
734
|
+
const audios = sortByKindSlot(upstreamOfKind(upstream, "audio").filter(({ targetInput }) => targetInput !== "replace_audio"), "audio");
|
|
735
|
+
const candidates = {
|
|
736
|
+
first_frame: outputsOf(firstFrame ? [firstFrame] : []),
|
|
737
|
+
last_frame: outputsOf(lastFrame ? [lastFrame] : []),
|
|
738
|
+
image: outputsOf(images),
|
|
739
|
+
video: outputsOf(videos),
|
|
740
|
+
audio: outputsOf(audios),
|
|
741
|
+
};
|
|
742
|
+
const refs = {};
|
|
743
|
+
for (const role of REF_ROLES) {
|
|
744
|
+
const list = candidates[role] ?? [];
|
|
745
|
+
if (list.length > 0 && modeRoles.has(role))
|
|
746
|
+
refs[role] = list;
|
|
747
|
+
}
|
|
748
|
+
return refs;
|
|
749
|
+
}
|
|
750
|
+
function mediaOptions(model, cell) {
|
|
751
|
+
const settings = cellGenerationSettings(cell);
|
|
752
|
+
const names = new Set(model.options.map((option) => option.name));
|
|
753
|
+
return Object.fromEntries(Object.entries(settings).filter(([name, value]) => names.has(name) && value !== undefined && value !== ""));
|
|
754
|
+
}
|
|
755
|
+
function mediaGenerationArgs(model, document, cell, bookId, engine) {
|
|
756
|
+
const upstream = upstreamValues(document, cell);
|
|
757
|
+
const mode = generationModeFor(model, document, cell, upstream);
|
|
758
|
+
const generationCell = { ...cell, inputs: { ...cell.inputs, generationMode: mode } };
|
|
759
|
+
const mentions = resolveGenerationInputReferences({
|
|
760
|
+
engine,
|
|
761
|
+
document,
|
|
762
|
+
cell: generationCell,
|
|
763
|
+
prompt: promptFor(document, generationCell),
|
|
764
|
+
});
|
|
765
|
+
const replacementAudio = upstream.find((entry) => entry.targetInput === "replace_audio" && typeof entry.output === "string");
|
|
766
|
+
const input = {
|
|
767
|
+
book_id: bookId,
|
|
768
|
+
model: model.id,
|
|
769
|
+
...(mentions.prompt ? { prompt: mentions.prompt } : {}),
|
|
770
|
+
refs: mediaRefsFor(model, upstream, mode),
|
|
771
|
+
options: mediaOptions(model, cell),
|
|
772
|
+
...(mode && mode !== "default" ? { mode } : {}),
|
|
773
|
+
...(replacementAudio?.output && model.media === "video" ? { replace_audio_with: replacementAudio.output } : {}),
|
|
774
|
+
};
|
|
775
|
+
return { input, mentions };
|
|
776
|
+
}
|
|
777
|
+
export function notebookCellModel(cell) {
|
|
778
|
+
if (cell.type !== "generate_image" && cell.type !== "generate_video" && cell.type !== "generate_audio")
|
|
779
|
+
return null;
|
|
780
|
+
if (cell.tool === "edit_image_subjects")
|
|
781
|
+
return null;
|
|
782
|
+
return cell.model ? tryResolveModel(cell.model.trim()) : null;
|
|
783
|
+
}
|
|
784
|
+
export function resolveNotebookGenerationTool(cell) {
|
|
785
|
+
const model = notebookCellModel(cell);
|
|
786
|
+
if (model)
|
|
787
|
+
return `generate_${model.media}`;
|
|
788
|
+
return cell.tool?.trim() || null;
|
|
789
|
+
}
|
|
790
|
+
function formatNotebookGenerationSchemaError(error) {
|
|
791
|
+
return error.issues.map((issue) => {
|
|
792
|
+
const label = issue.path
|
|
793
|
+
.map(String)
|
|
794
|
+
.join(".")
|
|
795
|
+
.replaceAll("_", " ")
|
|
796
|
+
.replace(/^./u, (character) => character.toUpperCase());
|
|
797
|
+
const input = "input" in issue ? issue.input : undefined;
|
|
798
|
+
const missing = issue.code === "invalid_type" && (input === undefined
|
|
799
|
+
|| input === null
|
|
800
|
+
|| (typeof input === "number" && Number.isNaN(input)));
|
|
801
|
+
const message = missing ? "Required" : issue.message;
|
|
802
|
+
return label ? `${label}: ${message}` : message;
|
|
803
|
+
}).join("; ");
|
|
804
|
+
}
|
|
805
|
+
function referenceIssueError(cell, mentions) {
|
|
806
|
+
const issues = [...mentions.unresolved, ...mentions.unconnected];
|
|
807
|
+
return issues.length > 0
|
|
808
|
+
? `Cannot prepare ${notebookGridTag(cell.slot)}: ${issues.map((reference) => reference.message).join("; ")}`
|
|
809
|
+
: null;
|
|
810
|
+
}
|
|
811
|
+
async function prepareMediaGeneration(model, document, cell, bookId, engine, options) {
|
|
812
|
+
const expected = cell.type.replace(/^generate_/u, "");
|
|
813
|
+
if (model.media !== expected) {
|
|
814
|
+
throw new Error(`${model.id} generates ${model.media}, not ${cell.type}`);
|
|
815
|
+
}
|
|
816
|
+
const contract = notebookGenerationInputContract(model);
|
|
817
|
+
const configuredMode = typeof cell.inputs?.generationMode === "string" ? cell.inputs.generationMode : undefined;
|
|
818
|
+
if (!notebookGenerationInputsAreCompatible(contract, document, cell, configuredMode)) {
|
|
819
|
+
const inputError = notebookGenerationInputError(contract, document, cell, configuredMode);
|
|
820
|
+
throw new Error(`${model.title}${configuredMode ? ` mode ${configuredMode}` : ""} does not accept the current media inputs for ${notebookGridTag(cell.slot)}${inputError ? `. ${inputError}` : ""}`);
|
|
821
|
+
}
|
|
822
|
+
const prepared = mediaGenerationArgs(model, document, cell, bookId, engine);
|
|
823
|
+
const referenceError = referenceIssueError(cell, prepared.mentions);
|
|
824
|
+
if (!options.allowReferenceIssues && referenceError)
|
|
825
|
+
throw new Error(referenceError);
|
|
826
|
+
if (!referenceError) {
|
|
827
|
+
const preview = previewGeneration(prepared.input);
|
|
828
|
+
if (!preview.ok)
|
|
829
|
+
throw new Error(preview.error);
|
|
830
|
+
const timingError = await checkInsertVideoTiming(engine, model, normalizeRefs(prepared.input.refs), preview.preview.payload.input ?? {});
|
|
831
|
+
if (timingError)
|
|
832
|
+
throw new Error(timingError);
|
|
833
|
+
}
|
|
834
|
+
return {
|
|
835
|
+
tool: `generate_${model.media}`,
|
|
836
|
+
provider: model.provider,
|
|
837
|
+
model: model.id,
|
|
838
|
+
input: prepared.input,
|
|
839
|
+
mentions: prepared.mentions,
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
async function prepareImageEditGeneration(engine, document, cell, options) {
|
|
843
|
+
const mentions = resolveGenerationInputReferences({ engine, document, cell, prompt: cell.prompt ?? "" });
|
|
844
|
+
const issues = [...mentions.unresolved, ...mentions.unconnected];
|
|
845
|
+
if (!options.allowReferenceIssues && issues.length > 0) {
|
|
846
|
+
throw new Error(issues.map((issue) => issue.message).join("; "));
|
|
847
|
+
}
|
|
848
|
+
const imageEdit = await prepareNotebookImageEdit(engine, document, cell, mentions.prompt);
|
|
849
|
+
return { tool: "edit_image_subjects", provider: imageEdit.provider, model: imageEdit.model,
|
|
850
|
+
input: imageEdit.input, mentions, imageEdit };
|
|
851
|
+
}
|
|
852
|
+
async function prepareToolGeneration(engine, document, cell, bookId, tool, options) {
|
|
853
|
+
const { getAllToolDefs } = await import("../tools/index.js");
|
|
854
|
+
const definition = getAllToolDefs(engine).find((candidate) => candidate.name === tool);
|
|
855
|
+
if (!definition)
|
|
856
|
+
throw new Error(`Unknown notebook generation tool: ${tool}`);
|
|
857
|
+
const prepared = toolArgsWithMentions(definition, document, cell, bookId, engine);
|
|
858
|
+
const referenceError = referenceIssueError(cell, prepared.mentions);
|
|
859
|
+
if (!options.allowReferenceIssues && referenceError)
|
|
860
|
+
throw new Error(referenceError);
|
|
861
|
+
const parsed = z.object(definition.schema).safeParse(prepared.input);
|
|
862
|
+
if (!parsed.success)
|
|
863
|
+
throw new Error(formatNotebookGenerationSchemaError(parsed.error));
|
|
864
|
+
return {
|
|
865
|
+
tool,
|
|
866
|
+
provider: cell.provider ?? "unknown",
|
|
867
|
+
model: cell.model ?? tool,
|
|
868
|
+
input: parsed.data,
|
|
869
|
+
mentions: prepared.mentions,
|
|
870
|
+
};
|
|
871
|
+
}
|
|
872
|
+
export async function prepareNotebookGeneration(engine, document, cell, bookId, options = {}) {
|
|
873
|
+
if (cell.tool === "edit_image_subjects")
|
|
874
|
+
return prepareImageEditGeneration(engine, document, cell, options);
|
|
875
|
+
const model = notebookCellModel(cell);
|
|
876
|
+
if (model)
|
|
877
|
+
return prepareMediaGeneration(model, document, cell, bookId, engine, options);
|
|
878
|
+
if (cell.type === "generate_image" || cell.type === "generate_video" || cell.type === "generate_audio") {
|
|
879
|
+
throw new Error(`${notebookGridTag(cell.slot)} uses ${cell.model || cell.tool || "no model"}, which is not a generation model; pick one from list_generation_models`);
|
|
880
|
+
}
|
|
881
|
+
const tool = resolveNotebookGenerationTool(cell);
|
|
882
|
+
if (!tool)
|
|
883
|
+
throw new Error(`No generation tool for ${cell.type} cell ${cell.id}`);
|
|
884
|
+
return prepareToolGeneration(engine, document, cell, bookId, tool, options);
|
|
885
|
+
}
|
|
886
|
+
const GENERATION_ERROR_MAX_LENGTH = 500;
|
|
887
|
+
function truncateGenerationError(message) {
|
|
888
|
+
return message.length > GENERATION_ERROR_MAX_LENGTH
|
|
889
|
+
? message.slice(0, GENERATION_ERROR_MAX_LENGTH)
|
|
890
|
+
: message;
|
|
891
|
+
}
|
|
892
|
+
async function recordGenerationDispatch(engine, args) {
|
|
893
|
+
try {
|
|
894
|
+
const result = await engine.generations.record(args);
|
|
895
|
+
if (!result.ok) {
|
|
896
|
+
logger.warn("generation-record-failed", { cellId: args.cellId, error: result.error });
|
|
897
|
+
return null;
|
|
898
|
+
}
|
|
899
|
+
return result.value.generationId;
|
|
900
|
+
}
|
|
901
|
+
catch (cause) {
|
|
902
|
+
logger.warn("generation-record-failed", { cellId: args.cellId, error: cause });
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async function recordGenerationPrompt(engine, cell, context) {
|
|
907
|
+
const surface = generationPromptSurface(cell.type);
|
|
908
|
+
if (!surface || !cell.prompt?.trim())
|
|
909
|
+
return;
|
|
910
|
+
try {
|
|
911
|
+
const result = await engine.prompts.record({ surface, prompt: cell.prompt, context });
|
|
912
|
+
if (!result.ok) {
|
|
913
|
+
logger.warn("prompt-record-failed", { cellId: cell.id, error: result.error });
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
catch (cause) {
|
|
917
|
+
logger.warn("prompt-record-failed", { cellId: cell.id, error: cause });
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
async function patchGeneration(engine, generationId, patch) {
|
|
921
|
+
if (!generationId)
|
|
922
|
+
return;
|
|
923
|
+
try {
|
|
924
|
+
const result = await engine.generations.update(generationId, patch);
|
|
925
|
+
if (!result.ok) {
|
|
926
|
+
logger.warn("generation-update-failed", { generationId, error: result.error });
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
catch (cause) {
|
|
930
|
+
logger.warn("generation-update-failed", { generationId, error: cause });
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
function activeGenerationIdForRun(engine, notebookId, cellId, runId, providerArtifactId) {
|
|
934
|
+
try {
|
|
935
|
+
const listed = engine.generations.listForCell(notebookId, cellId);
|
|
936
|
+
if (!listed.ok) {
|
|
937
|
+
logger.warn("generation-list-failed", { cellId, error: listed.error });
|
|
938
|
+
return null;
|
|
939
|
+
}
|
|
940
|
+
const match = listed.value.find((generation) => (generation.status === "dispatched"
|
|
941
|
+
|| generation.status === "awaiting_provider")
|
|
942
|
+
&& ((runId !== undefined && generation.runId === runId)
|
|
943
|
+
|| (providerArtifactId !== undefined
|
|
944
|
+
&& generation.providerArtifactId === providerArtifactId)));
|
|
945
|
+
return match?.generationId ?? null;
|
|
946
|
+
}
|
|
947
|
+
catch (cause) {
|
|
948
|
+
logger.warn("generation-list-failed", { cellId, error: cause });
|
|
949
|
+
return null;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
async function generationTool(engine, document, cell, bookId, options = {}) {
|
|
953
|
+
const prepared = await prepareNotebookGeneration(engine, document, cell, bookId);
|
|
954
|
+
const { getAllToolDefs } = await import("../tools/index.js");
|
|
955
|
+
const definition = getAllToolDefs(engine).find((candidate) => candidate.name === prepared.tool);
|
|
956
|
+
if (!definition) {
|
|
957
|
+
throw new Error(`Unknown notebook generation model: ${prepared.tool}`);
|
|
958
|
+
}
|
|
959
|
+
const resolvedPrompt = typeof prepared.input.prompt === "string"
|
|
960
|
+
? prepared.input.prompt
|
|
961
|
+
: "";
|
|
962
|
+
const outputCellId = options.outputCellId
|
|
963
|
+
?? findGenerationResultForRun(document, cell, {
|
|
964
|
+
runId: options.runId,
|
|
965
|
+
outputCellId: options.outputCellId,
|
|
966
|
+
})?.outputCell.id;
|
|
967
|
+
const generationId = await recordGenerationDispatch(engine, {
|
|
968
|
+
notebookId: document.id,
|
|
969
|
+
cellId: cell.id,
|
|
970
|
+
tool: prepared.tool,
|
|
971
|
+
status: "dispatched",
|
|
972
|
+
provider: prepared.provider,
|
|
973
|
+
model: prepared.model,
|
|
974
|
+
...(cell.prompt ? { prompt: cell.prompt } : {}),
|
|
975
|
+
...(resolvedPrompt ? { resolvedPrompt } : {}),
|
|
976
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
977
|
+
...(outputCellId ? { outputCellId } : {}),
|
|
978
|
+
});
|
|
979
|
+
await recordGenerationPrompt(engine, cell, {
|
|
980
|
+
tool: prepared.tool,
|
|
981
|
+
provider: prepared.provider,
|
|
982
|
+
model: prepared.model,
|
|
983
|
+
notebook_id: document.id,
|
|
984
|
+
cell_id: cell.id,
|
|
985
|
+
});
|
|
986
|
+
const fail = async (message) => {
|
|
987
|
+
await patchGeneration(engine, generationId, {
|
|
988
|
+
status: "failed",
|
|
989
|
+
error: truncateGenerationError(message),
|
|
990
|
+
});
|
|
991
|
+
throw new Error(message);
|
|
992
|
+
};
|
|
993
|
+
let response;
|
|
994
|
+
try {
|
|
995
|
+
if (prepared.imageEdit) {
|
|
996
|
+
if (!outputCellId)
|
|
997
|
+
throw new Error("Image edit has no notebook output cell");
|
|
998
|
+
const output = await engine.artifacts.create({ kind: "image", label: cell.label ?? "Edited image" });
|
|
999
|
+
if (!output.ok)
|
|
1000
|
+
throw new Error(output.error.message);
|
|
1001
|
+
const imageAssetId = output.value.artifactId;
|
|
1002
|
+
const jobId = `notebook-image-edit-${imageAssetId}`;
|
|
1003
|
+
await queue.enqueue(jobId, {
|
|
1004
|
+
...prepared.imageEdit.jobInput,
|
|
1005
|
+
type: "generate_image_subject_edit", imageAssetId, bookId,
|
|
1006
|
+
notebookId: document.id, producerCellId: cell.id, outputCellId, jobId,
|
|
1007
|
+
managedByNotebook: true,
|
|
1008
|
+
...(generationId ? { generationId } : {}),
|
|
1009
|
+
});
|
|
1010
|
+
response = { content: [{ type: "text", text: JSON.stringify({ image_asset_id: imageAssetId }) }] };
|
|
1011
|
+
}
|
|
1012
|
+
else {
|
|
1013
|
+
response = await definition.handler(prepared.input);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
catch (cause) {
|
|
1017
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1018
|
+
return fail(message);
|
|
1019
|
+
}
|
|
1020
|
+
if (response.isError) {
|
|
1021
|
+
return fail(response.content[0]?.text ?? `${prepared.tool} failed`);
|
|
1022
|
+
}
|
|
1023
|
+
const reference = artifactReference(textResult(response));
|
|
1024
|
+
if (!reference) {
|
|
1025
|
+
return fail(`${prepared.tool} did not return an output artifact`);
|
|
1026
|
+
}
|
|
1027
|
+
const artifact = engine.artifacts.get(reference);
|
|
1028
|
+
if (!artifact.ok)
|
|
1029
|
+
return fail(artifact.error.message);
|
|
1030
|
+
await patchGeneration(engine, generationId, {
|
|
1031
|
+
status: "awaiting_provider",
|
|
1032
|
+
providerArtifactId: artifact.value.artifactId,
|
|
1033
|
+
});
|
|
1034
|
+
return {
|
|
1035
|
+
artifactId: artifact.value.artifactId,
|
|
1036
|
+
tool: prepared.tool,
|
|
1037
|
+
provider: prepared.provider,
|
|
1038
|
+
model: prepared.model,
|
|
1039
|
+
resolvedPrompt,
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
1042
|
+
async function rewritePromptCell(engine, document, cell, bookId) {
|
|
1043
|
+
const prepared = await prepareNotebookGeneration(engine, document, cell, bookId);
|
|
1044
|
+
if (prepared.tool !== "rewrite_prompt") {
|
|
1045
|
+
throw new Error(`Note ${cell.id} must use rewrite_prompt`);
|
|
1046
|
+
}
|
|
1047
|
+
const { getAllToolDefs } = await import("../tools/index.js");
|
|
1048
|
+
const definition = getAllToolDefs(engine).find((candidate) => candidate.name === prepared.tool);
|
|
1049
|
+
if (!definition)
|
|
1050
|
+
throw new Error("rewrite_prompt is unavailable");
|
|
1051
|
+
const response = await definition.handler(prepared.input);
|
|
1052
|
+
if (response.isError) {
|
|
1053
|
+
throw new Error(response.content[0]?.text ?? "rewrite_prompt failed");
|
|
1054
|
+
}
|
|
1055
|
+
const value = textResult(response);
|
|
1056
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1057
|
+
throw new Error("rewrite_prompt returned an invalid result");
|
|
1058
|
+
}
|
|
1059
|
+
const rawResult = value;
|
|
1060
|
+
const resolvedPrompt = typeof rawResult.rewritten_prompt === "string"
|
|
1061
|
+
? rawResult.rewritten_prompt.trim()
|
|
1062
|
+
: "";
|
|
1063
|
+
if (!resolvedPrompt) {
|
|
1064
|
+
throw new Error("rewrite_prompt did not return rewritten_prompt");
|
|
1065
|
+
}
|
|
1066
|
+
const artifact = await engine.artifacts.create({
|
|
1067
|
+
kind: "script",
|
|
1068
|
+
label: `script ${notebookGridTag(cell.slot)} resolved prompt`,
|
|
1069
|
+
});
|
|
1070
|
+
if (!artifact.ok)
|
|
1071
|
+
throw new Error(artifact.error.message);
|
|
1072
|
+
const written = await engine.files.write(artifact.value.artifactId, "prompt.json", JSON.stringify({
|
|
1073
|
+
version: 1,
|
|
1074
|
+
originalPrompt: cell.prompt ?? "",
|
|
1075
|
+
resolvedPrompt,
|
|
1076
|
+
request: prepared.input,
|
|
1077
|
+
result: rawResult,
|
|
1078
|
+
featureId: cell.inputs?.featureId ?? null,
|
|
1079
|
+
shotContext: cell.inputs?.shotContext ?? null,
|
|
1080
|
+
audioContext: cell.inputs?.audioContext ?? null,
|
|
1081
|
+
resolvedAt: new Date().toISOString(),
|
|
1082
|
+
}, null, 2));
|
|
1083
|
+
if (!written.ok)
|
|
1084
|
+
throw new Error(written.error.message);
|
|
1085
|
+
return {
|
|
1086
|
+
artifactId: artifact.value.artifactId,
|
|
1087
|
+
tool: prepared.tool,
|
|
1088
|
+
resolvedPrompt,
|
|
1089
|
+
rawResult,
|
|
1090
|
+
};
|
|
1091
|
+
}
|
|
1092
|
+
async function failedArtifact(engine, cell, message) {
|
|
1093
|
+
const kind = notebookCellMediaType(cell) ?? "script";
|
|
1094
|
+
const result = await engine.artifacts.create({
|
|
1095
|
+
kind,
|
|
1096
|
+
label: kind === "script"
|
|
1097
|
+
? `script ${notebookGridTag(cell.slot)} failed`
|
|
1098
|
+
: `${notebookGridTag(cell.slot)} failed`,
|
|
1099
|
+
});
|
|
1100
|
+
if (!result.ok)
|
|
1101
|
+
throw new Error(result.error.message);
|
|
1102
|
+
const written = await engine.files.write(result.value.artifactId, "error.json", JSON.stringify({ message, failedAt: new Date().toISOString() }));
|
|
1103
|
+
if (!written.ok)
|
|
1104
|
+
throw new Error(written.error.message);
|
|
1105
|
+
return result.value.artifactId;
|
|
1106
|
+
}
|
|
1107
|
+
async function assembleCanonicalSequence(engine, document, cell) {
|
|
1108
|
+
const orderedClips = upstreamValues(document, cell)
|
|
1109
|
+
.filter((entry) => /^clip:\d+$/.test(entry.targetInput))
|
|
1110
|
+
.sort((left, right) => Number(left.targetInput.slice(5)) - Number(right.targetInput.slice(5)));
|
|
1111
|
+
if (orderedClips.length < 2) {
|
|
1112
|
+
throw new Error(`Concat cell ${cell.id} requires at least two clip inputs`);
|
|
1113
|
+
}
|
|
1114
|
+
for (let index = 0; index < orderedClips.length; index += 1) {
|
|
1115
|
+
const entry = orderedClips[index];
|
|
1116
|
+
if (entry.targetInput !== `clip:${index}`) {
|
|
1117
|
+
throw new Error(`Concat cell ${cell.id} requires contiguous clip:0 through clip:${orderedClips.length - 1}`);
|
|
1118
|
+
}
|
|
1119
|
+
if (!entry.output || notebookCellMediaType(entry.cell) !== "video") {
|
|
1120
|
+
throw new Error(`Concat input ${entry.targetInput} must be a completed video`);
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
const clips = orderedClips.map((entry) => entry.output);
|
|
1124
|
+
const audioId = firstUpstreamArtifact(document, cell, "audio")
|
|
1125
|
+
?? firstUpstreamArtifact(document, cell, "source-audio");
|
|
1126
|
+
let primary = engine.sequences.getPrimary();
|
|
1127
|
+
const videoTrack = primary.tracks.find((track) => track.kind === "video" && track.ordinal === 0)
|
|
1128
|
+
?? primary.tracks.find((track) => track.kind === "video");
|
|
1129
|
+
if (!videoTrack) {
|
|
1130
|
+
throw new Error("Primary sequence has no video track for assembly");
|
|
1131
|
+
}
|
|
1132
|
+
if (!primary.tracks.some((track) => track.kind === "caption")) {
|
|
1133
|
+
const added = await engine.sequences.addTrack(primary.sequenceId, {
|
|
1134
|
+
kind: "caption",
|
|
1135
|
+
name: "Feature captions",
|
|
1136
|
+
});
|
|
1137
|
+
if (!added.ok)
|
|
1138
|
+
throw new Error(added.error.message);
|
|
1139
|
+
primary = engine.sequences.getPrimary();
|
|
1140
|
+
}
|
|
1141
|
+
const activeVideoTrack = primary.tracks.find((track) => track.trackId === videoTrack.trackId);
|
|
1142
|
+
if (!activeVideoTrack || activeVideoTrack.kind !== "video") {
|
|
1143
|
+
throw new Error("Primary video track changed during assembly");
|
|
1144
|
+
}
|
|
1145
|
+
const audioTrack = primary.tracks.find((track) => track.kind === "audio");
|
|
1146
|
+
const captionTrack = primary.tracks.find((track) => track.kind === "caption");
|
|
1147
|
+
const fps = primary.frameRate.numerator / primary.frameRate.denominator;
|
|
1148
|
+
const placements = [];
|
|
1149
|
+
const clipDurations = [];
|
|
1150
|
+
let timelineStartFrame = 0;
|
|
1151
|
+
for (const clipId of clips) {
|
|
1152
|
+
const manifest = await engine.files.manifest(clipId);
|
|
1153
|
+
if (!manifest.ok)
|
|
1154
|
+
throw new Error(manifest.error.message);
|
|
1155
|
+
const file = manifest.value.files.find((entry) => entry.name.startsWith("original."))
|
|
1156
|
+
?? manifest.value.files[0];
|
|
1157
|
+
if (!file)
|
|
1158
|
+
throw new Error(`Clip artifact ${clipId} has no files`);
|
|
1159
|
+
let durationFrames = 90;
|
|
1160
|
+
const streams = engine.streams.list(clipId);
|
|
1161
|
+
if (streams.ok) {
|
|
1162
|
+
const videoStream = streams.value.find((stream) => stream.kind === "video")
|
|
1163
|
+
?? streams.value[0];
|
|
1164
|
+
if (videoStream) {
|
|
1165
|
+
const seconds = videoStream.durationTicks
|
|
1166
|
+
* videoStream.timeBase.numerator
|
|
1167
|
+
/ videoStream.timeBase.denominator;
|
|
1168
|
+
durationFrames = Math.max(1, Math.round(seconds * fps));
|
|
1169
|
+
placements.push({
|
|
1170
|
+
trackId: activeVideoTrack.trackId,
|
|
1171
|
+
timelineStartFrame,
|
|
1172
|
+
durationFrames,
|
|
1173
|
+
source: {
|
|
1174
|
+
kind: "timed",
|
|
1175
|
+
artifactId: clipId,
|
|
1176
|
+
range: {
|
|
1177
|
+
streamId: videoStream.streamId,
|
|
1178
|
+
objectHash: videoStream.objectHash,
|
|
1179
|
+
startTick: 0,
|
|
1180
|
+
durationTicks: videoStream.durationTicks,
|
|
1181
|
+
timeBase: videoStream.timeBase,
|
|
1182
|
+
},
|
|
1183
|
+
},
|
|
1184
|
+
speed: { numerator: 1, denominator: 1 },
|
|
1185
|
+
reverse: false,
|
|
1186
|
+
audioPolicy: "mute",
|
|
1187
|
+
audio: {
|
|
1188
|
+
gainDb: 0,
|
|
1189
|
+
muted: true,
|
|
1190
|
+
fadeInFrames: 0,
|
|
1191
|
+
fadeOutFrames: 0,
|
|
1192
|
+
},
|
|
1193
|
+
});
|
|
1194
|
+
clipDurations.push(durationFrames);
|
|
1195
|
+
timelineStartFrame += durationFrames;
|
|
1196
|
+
continue;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
placements.push({
|
|
1200
|
+
trackId: activeVideoTrack.trackId,
|
|
1201
|
+
timelineStartFrame,
|
|
1202
|
+
durationFrames,
|
|
1203
|
+
source: {
|
|
1204
|
+
kind: "still",
|
|
1205
|
+
artifactId: clipId,
|
|
1206
|
+
sourcePath: file.name,
|
|
1207
|
+
objectHash: file.objectHash,
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
clipDurations.push(durationFrames);
|
|
1211
|
+
timelineStartFrame += durationFrames;
|
|
1212
|
+
}
|
|
1213
|
+
if (audioId && audioTrack) {
|
|
1214
|
+
const audioStreams = engine.streams.list(audioId);
|
|
1215
|
+
if (audioStreams.ok) {
|
|
1216
|
+
const audioStream = audioStreams.value.find((stream) => stream.kind === "audio");
|
|
1217
|
+
if (audioStream) {
|
|
1218
|
+
const seconds = audioStream.durationTicks
|
|
1219
|
+
* audioStream.timeBase.numerator
|
|
1220
|
+
/ audioStream.timeBase.denominator;
|
|
1221
|
+
placements.push({
|
|
1222
|
+
trackId: audioTrack.trackId,
|
|
1223
|
+
timelineStartFrame: 0,
|
|
1224
|
+
durationFrames: Math.min(timelineStartFrame, Math.max(1, Math.round(seconds * fps))),
|
|
1225
|
+
source: {
|
|
1226
|
+
kind: "timed",
|
|
1227
|
+
artifactId: audioId,
|
|
1228
|
+
range: {
|
|
1229
|
+
streamId: audioStream.streamId,
|
|
1230
|
+
objectHash: audioStream.objectHash,
|
|
1231
|
+
startTick: 0,
|
|
1232
|
+
durationTicks: audioStream.durationTicks,
|
|
1233
|
+
timeBase: audioStream.timeBase,
|
|
1234
|
+
},
|
|
1235
|
+
},
|
|
1236
|
+
speed: { numerator: 1, denominator: 1 },
|
|
1237
|
+
reverse: false,
|
|
1238
|
+
audioPolicy: "preserve-pitch",
|
|
1239
|
+
audio: {
|
|
1240
|
+
gainDb: 0,
|
|
1241
|
+
muted: false,
|
|
1242
|
+
fadeInFrames: 0,
|
|
1243
|
+
fadeOutFrames: 0,
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
}
|
|
1249
|
+
const targetTrackIds = [
|
|
1250
|
+
activeVideoTrack.trackId,
|
|
1251
|
+
...(audioTrack ? [audioTrack.trackId] : []),
|
|
1252
|
+
];
|
|
1253
|
+
const occupiedDuration = primary.clips
|
|
1254
|
+
.filter((clip) => targetTrackIds.includes(clip.trackId))
|
|
1255
|
+
.reduce((maximum, clip) => Math.max(maximum, clip.timelineStartFrame + clip.durationFrames), 0);
|
|
1256
|
+
const previousDuration = Number(cell.inputs?.assemblyDurationFrames ?? 0);
|
|
1257
|
+
const replacementDuration = Math.max(1, occupiedDuration, previousDuration, timelineStartFrame);
|
|
1258
|
+
const operations = [{
|
|
1259
|
+
kind: "batch-replace-range",
|
|
1260
|
+
range: {
|
|
1261
|
+
sequenceId: primary.sequenceId,
|
|
1262
|
+
startFrame: 0,
|
|
1263
|
+
durationFrames: replacementDuration,
|
|
1264
|
+
},
|
|
1265
|
+
trackIds: targetTrackIds,
|
|
1266
|
+
placements,
|
|
1267
|
+
ripple: false,
|
|
1268
|
+
}];
|
|
1269
|
+
const captionTexts = Array.isArray(cell.inputs?.captions)
|
|
1270
|
+
? cell.inputs.captions.filter((value) => typeof value === "string" && value.trim().length > 0)
|
|
1271
|
+
: [];
|
|
1272
|
+
if (captionTrack) {
|
|
1273
|
+
let captionStartFrame = 0;
|
|
1274
|
+
for (let index = 0; index < clipDurations.length; index += 1) {
|
|
1275
|
+
const durationFrames = clipDurations[index];
|
|
1276
|
+
const text = captionTexts[index];
|
|
1277
|
+
if (text) {
|
|
1278
|
+
operations.push({
|
|
1279
|
+
kind: "upsert-caption-cue",
|
|
1280
|
+
cue: {
|
|
1281
|
+
cueId: uuidv5(`${primary.sequenceId}:notebook-feature-caption:${index}`, uuidv5.URL),
|
|
1282
|
+
trackId: captionTrack.trackId,
|
|
1283
|
+
timelineStartFrame: captionStartFrame,
|
|
1284
|
+
durationFrames,
|
|
1285
|
+
text,
|
|
1286
|
+
styleId: "high-contrast",
|
|
1287
|
+
},
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1290
|
+
captionStartFrame += durationFrames;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
const intent = {
|
|
1294
|
+
intentVersion: MVP_CONTRACT_VERSION,
|
|
1295
|
+
commandId: uuidv7(),
|
|
1296
|
+
sequenceId: primary.sequenceId,
|
|
1297
|
+
baseRevision: engine.head,
|
|
1298
|
+
actor: "notebook-assembly",
|
|
1299
|
+
sourceSurface: "system",
|
|
1300
|
+
confirmationPolicy: "reversible-single-step",
|
|
1301
|
+
operations,
|
|
1302
|
+
};
|
|
1303
|
+
const preview = engine.edits.preview(intent);
|
|
1304
|
+
if (!preview.ok)
|
|
1305
|
+
throw new Error(preview.error.message);
|
|
1306
|
+
const commit = await engine.edits.commit(intent, preview.value.previewHash);
|
|
1307
|
+
if (!commit.ok)
|
|
1308
|
+
throw new Error(commit.error.message);
|
|
1309
|
+
const sequenceRevision = engine.head;
|
|
1310
|
+
const script = await engine.artifacts.create({
|
|
1311
|
+
kind: "script",
|
|
1312
|
+
label: `${notebookGridTag(cell.slot)} assembly`,
|
|
1313
|
+
});
|
|
1314
|
+
if (!script.ok)
|
|
1315
|
+
throw new Error(script.error.message);
|
|
1316
|
+
const written = await engine.files.write(script.value.artifactId, "assembly.json", JSON.stringify({
|
|
1317
|
+
sequenceId: primary.sequenceId,
|
|
1318
|
+
sequenceRevision,
|
|
1319
|
+
clipArtifactIds: clips,
|
|
1320
|
+
audioArtifactId: audioId,
|
|
1321
|
+
assemblyDurationFrames: timelineStartFrame,
|
|
1322
|
+
captionTexts,
|
|
1323
|
+
assembledAt: new Date().toISOString(),
|
|
1324
|
+
operationCount: operations.length,
|
|
1325
|
+
}));
|
|
1326
|
+
if (!written.ok)
|
|
1327
|
+
throw new Error(written.error.message);
|
|
1328
|
+
return {
|
|
1329
|
+
scriptArtifactId: script.value.artifactId,
|
|
1330
|
+
sequenceRevision,
|
|
1331
|
+
sequenceId: primary.sequenceId,
|
|
1332
|
+
clipArtifactIds: clips,
|
|
1333
|
+
audioArtifactId: audioId,
|
|
1334
|
+
assemblyDurationFrames: timelineStartFrame,
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
async function executeLocalOrGeneration(engine, bookId, document, cell, options = {}) {
|
|
1338
|
+
if (cell.type === "split_video" && cell.operation === "split_ranges") {
|
|
1339
|
+
const sourceId = requiredVideoInputArtifact(document, cell);
|
|
1340
|
+
const groups = parseSplitOutputGroups(cell.inputs?.outputsSeconds);
|
|
1341
|
+
if (groups.length === 0) {
|
|
1342
|
+
throw new Error(`split_video cell ${cell.id} requires outputsSeconds range groups`);
|
|
1343
|
+
}
|
|
1344
|
+
for (const group of groups) {
|
|
1345
|
+
const total = group.reduce((sum, range) => sum + (range.endSeconds - range.startSeconds), 0);
|
|
1346
|
+
if (total < MINIMUM_SPLIT_PIECE_SECONDS) {
|
|
1347
|
+
throw new Error(`split_video cell ${cell.id} requires every output range to be at least ${MINIMUM_SPLIT_PIECE_SECONDS}s`);
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
const outputCellIds = requiredIndexedSplitVideoOutputCellIds(document, cell, groups.length);
|
|
1351
|
+
const results = [];
|
|
1352
|
+
for (const [index, group] of groups.entries()) {
|
|
1353
|
+
const title = `${notebookGridTag(cell.slot)} output ${String(index + 1).padStart(2, "0")}`;
|
|
1354
|
+
const result = group.length === 1
|
|
1355
|
+
? await splitSourceRange(engine, sourceId, group[0], title)
|
|
1356
|
+
: await concatSourceRanges(engine, sourceId, group, title);
|
|
1357
|
+
results.push(result);
|
|
1358
|
+
}
|
|
1359
|
+
const [firstFrame, lastFrame] = await Promise.all([
|
|
1360
|
+
captureFrame(engine, results[0].artifactId, {
|
|
1361
|
+
role: "first",
|
|
1362
|
+
title: `${notebookGridTag(cell.slot)} · first frame`,
|
|
1363
|
+
}),
|
|
1364
|
+
captureFrame(engine, results.at(-1).artifactId, {
|
|
1365
|
+
role: "last",
|
|
1366
|
+
title: `${notebookGridTag(cell.slot)} · last frame`,
|
|
1367
|
+
}),
|
|
1368
|
+
]);
|
|
1369
|
+
let withOutputs = document;
|
|
1370
|
+
for (const [index, result] of results.entries()) {
|
|
1371
|
+
withOutputs = withMediaOutput(withOutputs, outputCellIds[index], result.artifactId, { durationSeconds: result.durationSeconds });
|
|
1372
|
+
}
|
|
1373
|
+
return {
|
|
1374
|
+
artifactId: results[0].artifactId,
|
|
1375
|
+
tool: "ffmpeg",
|
|
1376
|
+
document: withOutputs,
|
|
1377
|
+
cellInputs: {
|
|
1378
|
+
durationSeconds: results.reduce((total, result) => total + result.durationSeconds, 0),
|
|
1379
|
+
firstFrameArtifactId: firstFrame.artifactId,
|
|
1380
|
+
lastFrameArtifactId: lastFrame.artifactId,
|
|
1381
|
+
},
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
if (cell.type === "split_video" && cell.operation === "split_at_points") {
|
|
1385
|
+
const sourceId = requiredVideoInputArtifact(document, cell);
|
|
1386
|
+
const rawCutPoints = cell.inputs?.cutPointsSeconds;
|
|
1387
|
+
const sourceDurationSeconds = Number(cell.inputs?.sourceDurationSeconds ?? cell.inputs?.endSeconds);
|
|
1388
|
+
if (!Array.isArray(rawCutPoints)
|
|
1389
|
+
|| rawCutPoints.length === 0
|
|
1390
|
+
|| !Number.isFinite(sourceDurationSeconds)) {
|
|
1391
|
+
throw new Error(`split_video cell ${cell.id} requires cut points and a source duration`);
|
|
1392
|
+
}
|
|
1393
|
+
const cutPoints = rawCutPoints.map((value) => typeof value === "number" ? value : Number.NaN);
|
|
1394
|
+
let previous = 0;
|
|
1395
|
+
for (const cutPoint of cutPoints) {
|
|
1396
|
+
if (!Number.isFinite(cutPoint)
|
|
1397
|
+
|| cutPoint <= previous
|
|
1398
|
+
|| sourceDurationSeconds - cutPoint < MINIMUM_SPLIT_PIECE_SECONDS
|
|
1399
|
+
|| cutPoint - previous < MINIMUM_SPLIT_PIECE_SECONDS) {
|
|
1400
|
+
throw new Error(`split_video cell ${cell.id} requires ordered cut points at least ${MINIMUM_SPLIT_PIECE_SECONDS}s apart`);
|
|
1401
|
+
}
|
|
1402
|
+
previous = cutPoint;
|
|
1403
|
+
}
|
|
1404
|
+
const boundaries = [0, ...cutPoints, sourceDurationSeconds];
|
|
1405
|
+
const ranges = boundaries.slice(0, -1).map((startSeconds, index) => ({
|
|
1406
|
+
endSeconds: boundaries[index + 1],
|
|
1407
|
+
startSeconds,
|
|
1408
|
+
}));
|
|
1409
|
+
const outputCellIds = requiredIndexedSplitVideoOutputCellIds(document, cell, ranges.length);
|
|
1410
|
+
const results = await splitSourceRanges(engine, sourceId, ranges.map((range, index) => ({
|
|
1411
|
+
range,
|
|
1412
|
+
title: `${notebookGridTag(cell.slot)} chunk ${String(index + 1).padStart(2, "0")}`,
|
|
1413
|
+
})), 2);
|
|
1414
|
+
const [firstFrame, lastFrame] = await Promise.all([
|
|
1415
|
+
captureFrame(engine, results[0].artifactId, {
|
|
1416
|
+
role: "first",
|
|
1417
|
+
title: `${notebookGridTag(cell.slot)} · first frame`,
|
|
1418
|
+
}),
|
|
1419
|
+
captureFrame(engine, results.at(-1).artifactId, {
|
|
1420
|
+
role: "last",
|
|
1421
|
+
title: `${notebookGridTag(cell.slot)} · last frame`,
|
|
1422
|
+
}),
|
|
1423
|
+
]);
|
|
1424
|
+
let withOutputs = document;
|
|
1425
|
+
for (const [index, result] of results.entries()) {
|
|
1426
|
+
withOutputs = withMediaOutput(withOutputs, outputCellIds[index], result.artifactId, { durationSeconds: result.durationSeconds });
|
|
1427
|
+
}
|
|
1428
|
+
return {
|
|
1429
|
+
artifactId: results[0].artifactId,
|
|
1430
|
+
tool: "ffmpeg",
|
|
1431
|
+
document: withOutputs,
|
|
1432
|
+
cellInputs: {
|
|
1433
|
+
durationSeconds: results.reduce((total, result) => total + result.durationSeconds, 0),
|
|
1434
|
+
firstFrameArtifactId: firstFrame.artifactId,
|
|
1435
|
+
lastFrameArtifactId: lastFrame.artifactId,
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1438
|
+
}
|
|
1439
|
+
if (cell.type === "split_video"
|
|
1440
|
+
&& cell.operation === "split_range"
|
|
1441
|
+
&& typeof cell.inputs?.splitAtSeconds === "number") {
|
|
1442
|
+
const sourceId = requiredVideoInputArtifact(document, cell);
|
|
1443
|
+
const splitAtSeconds = cell.inputs.splitAtSeconds;
|
|
1444
|
+
const sourceDurationSeconds = Number(cell.inputs.sourceDurationSeconds ?? cell.inputs.endSeconds);
|
|
1445
|
+
if (!Number.isFinite(splitAtSeconds)
|
|
1446
|
+
|| !Number.isFinite(sourceDurationSeconds)
|
|
1447
|
+
|| !(splitAtSeconds > 0)
|
|
1448
|
+
|| !(sourceDurationSeconds > splitAtSeconds)) {
|
|
1449
|
+
throw new Error(`split_video cell ${cell.id} requires a split boundary within the source duration`);
|
|
1450
|
+
}
|
|
1451
|
+
const outputCellIds = requiredTwoWaySplitVideoOutputCellIds(document, cell);
|
|
1452
|
+
const [first, remaining] = await Promise.all([
|
|
1453
|
+
splitSourceRange(engine, sourceId, { startSeconds: 0, endSeconds: splitAtSeconds }, `${notebookGridTag(cell.slot)} first`),
|
|
1454
|
+
splitSourceRange(engine, sourceId, {
|
|
1455
|
+
startSeconds: splitAtSeconds,
|
|
1456
|
+
endSeconds: sourceDurationSeconds,
|
|
1457
|
+
}, `${notebookGridTag(cell.slot)} remaining`),
|
|
1458
|
+
]);
|
|
1459
|
+
const [firstFrame, lastFrame] = await Promise.all([
|
|
1460
|
+
captureFrame(engine, first.artifactId, {
|
|
1461
|
+
role: "first",
|
|
1462
|
+
title: `${notebookGridTag(cell.slot)} · first frame`,
|
|
1463
|
+
}),
|
|
1464
|
+
captureFrame(engine, remaining.artifactId, {
|
|
1465
|
+
role: "last",
|
|
1466
|
+
title: `${notebookGridTag(cell.slot)} · last frame`,
|
|
1467
|
+
}),
|
|
1468
|
+
]);
|
|
1469
|
+
const withFirstOutput = withMediaOutput(document, outputCellIds.first, first.artifactId, { durationSeconds: first.durationSeconds });
|
|
1470
|
+
return {
|
|
1471
|
+
artifactId: first.artifactId,
|
|
1472
|
+
tool: "ffmpeg",
|
|
1473
|
+
document: withMediaOutput(withFirstOutput, outputCellIds.remaining, remaining.artifactId, { durationSeconds: remaining.durationSeconds }),
|
|
1474
|
+
cellInputs: {
|
|
1475
|
+
durationSeconds: first.durationSeconds + remaining.durationSeconds,
|
|
1476
|
+
firstFrameArtifactId: firstFrame.artifactId,
|
|
1477
|
+
lastFrameArtifactId: lastFrame.artifactId,
|
|
1478
|
+
},
|
|
1479
|
+
};
|
|
1480
|
+
}
|
|
1481
|
+
if (cell.type === "split_video" && cell.operation === "split_range") {
|
|
1482
|
+
const sourceId = requiredVideoInputArtifact(document, cell);
|
|
1483
|
+
const videoCellIds = requiredMediaOutputCellIds(document, cell, "video");
|
|
1484
|
+
const frameCellIds = splitFrameOutputCellIds(document, videoCellIds);
|
|
1485
|
+
const startSeconds = Number(cell.inputs?.startSeconds ?? 0);
|
|
1486
|
+
const endSeconds = Number(cell.inputs?.endSeconds ?? 0);
|
|
1487
|
+
const result = await splitSourceRange(engine, sourceId, { startSeconds, endSeconds }, notebookGridTag(cell.slot));
|
|
1488
|
+
const firstFrame = await captureFrame(engine, result.artifactId, {
|
|
1489
|
+
role: "first",
|
|
1490
|
+
title: `${notebookGridTag(cell.slot)} · first frame`,
|
|
1491
|
+
});
|
|
1492
|
+
const lastFrame = await captureFrame(engine, result.artifactId, {
|
|
1493
|
+
role: "last",
|
|
1494
|
+
title: `${notebookGridTag(cell.slot)} · last frame`,
|
|
1495
|
+
});
|
|
1496
|
+
return {
|
|
1497
|
+
artifactId: result.artifactId,
|
|
1498
|
+
tool: "ffmpeg",
|
|
1499
|
+
document: withSplitOutputs(document, {
|
|
1500
|
+
video: videoCellIds,
|
|
1501
|
+
first: frameCellIds.first,
|
|
1502
|
+
last: frameCellIds.last,
|
|
1503
|
+
}, {
|
|
1504
|
+
videoArtifactId: result.artifactId,
|
|
1505
|
+
firstFrameArtifactId: firstFrame.artifactId,
|
|
1506
|
+
lastFrameArtifactId: lastFrame.artifactId,
|
|
1507
|
+
durationSeconds: result.durationSeconds,
|
|
1508
|
+
firstAtSeconds: firstFrame.atSeconds,
|
|
1509
|
+
lastAtSeconds: lastFrame.atSeconds,
|
|
1510
|
+
}),
|
|
1511
|
+
cellInputs: {
|
|
1512
|
+
durationSeconds: result.durationSeconds,
|
|
1513
|
+
firstFrameArtifactId: firstFrame.artifactId,
|
|
1514
|
+
lastFrameArtifactId: lastFrame.artifactId,
|
|
1515
|
+
},
|
|
1516
|
+
};
|
|
1517
|
+
}
|
|
1518
|
+
if ((cell.type === "extract_audio" && cell.operation === "extract_audio")
|
|
1519
|
+
|| cell.inputs?.mediaRole === "extract-audio-output") {
|
|
1520
|
+
const sourceId = requiredVideoInputArtifact(document, cell);
|
|
1521
|
+
const previous = cell.outputArtifactId && cell.inputs?.audioRange === undefined
|
|
1522
|
+
? await engine.metadata.artifacts.read(cell.outputArtifactId, "original")
|
|
1523
|
+
: undefined;
|
|
1524
|
+
const previousRange = previous?.ok && previous.value.origin === "extract_audio"
|
|
1525
|
+
&& previous.value.sourceArtifactId === sourceId && typeof previous.value.startSeconds === "number"
|
|
1526
|
+
? previous.value : undefined;
|
|
1527
|
+
const savedRange = cell.inputs?.audioRange ?? previousRange;
|
|
1528
|
+
const rangeInputs = savedRange && typeof savedRange === "object" && !Array.isArray(savedRange)
|
|
1529
|
+
? savedRange
|
|
1530
|
+
: undefined;
|
|
1531
|
+
const range = rangeInputs === undefined ? undefined : {
|
|
1532
|
+
startSeconds: Number(rangeInputs.startSeconds ?? 0),
|
|
1533
|
+
...(rangeInputs.durationSeconds === undefined ? {} : { durationSeconds: Number(rangeInputs.durationSeconds) }),
|
|
1534
|
+
};
|
|
1535
|
+
const result = await extractSourceAudio(engine, sourceId, notebookGridTag(cell.slot), range);
|
|
1536
|
+
return {
|
|
1537
|
+
artifactId: result.artifactId,
|
|
1538
|
+
tool: "ffmpeg",
|
|
1539
|
+
cellInputs: {
|
|
1540
|
+
durationSeconds: result.durationSeconds,
|
|
1541
|
+
outputVersionArtifactIds: withOutputVersionIds(cell, result.artifactId),
|
|
1542
|
+
state: "completed",
|
|
1543
|
+
},
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
if (cell.type === "analyze" && cell.operation === "analyze_source") {
|
|
1547
|
+
const sourceId = firstUpstreamArtifact(document, cell, "source")
|
|
1548
|
+
?? firstUpstreamArtifact(document, cell);
|
|
1549
|
+
if (!sourceId)
|
|
1550
|
+
throw new Error(`Analyze cell ${cell.id} has no source artifact`);
|
|
1551
|
+
const fixture = analysisFixtureForCell(cell);
|
|
1552
|
+
const configuredDuration = Number(cell.inputs?.sourceDurationSeconds
|
|
1553
|
+
?? (fixture && typeof fixture === "object"
|
|
1554
|
+
? fixture.sourceDurationSeconds
|
|
1555
|
+
: undefined));
|
|
1556
|
+
const analysisRun = fixture !== null
|
|
1557
|
+
? {
|
|
1558
|
+
rawOutput: fixture,
|
|
1559
|
+
sourceDurationSeconds: Number.isFinite(configuredDuration)
|
|
1560
|
+
? configuredDuration
|
|
1561
|
+
: 60,
|
|
1562
|
+
request: {
|
|
1563
|
+
provider: "openai",
|
|
1564
|
+
model: INTELLIGENCE_MODEL,
|
|
1565
|
+
reasoning: { effort: ANALYSIS_REASONING_EFFORT },
|
|
1566
|
+
mode: "fixture",
|
|
1567
|
+
prompt: cell.prompt ?? "",
|
|
1568
|
+
},
|
|
1569
|
+
}
|
|
1570
|
+
: await analyzeNotebookSource(engine, sourceId, cell.prompt ?? "", {
|
|
1571
|
+
...(Number.isFinite(configuredDuration)
|
|
1572
|
+
? { sourceDurationSeconds: configuredDuration }
|
|
1573
|
+
: {}),
|
|
1574
|
+
});
|
|
1575
|
+
if (!Number.isFinite(analysisRun.sourceDurationSeconds)
|
|
1576
|
+
|| analysisRun.sourceDurationSeconds <= 0) {
|
|
1577
|
+
throw new Error(`Analyze cell ${cell.id} requires a positive source duration`);
|
|
1578
|
+
}
|
|
1579
|
+
const next = await applyAnalysisResults(engine, document, cell.id, {
|
|
1580
|
+
rawOutput: analysisRun.rawOutput,
|
|
1581
|
+
sourceDurationSeconds: analysisRun.sourceDurationSeconds,
|
|
1582
|
+
request: analysisRun.request,
|
|
1583
|
+
sourceArtifactId: sourceId,
|
|
1584
|
+
...("llmExchanges" in analysisRun
|
|
1585
|
+
? { llmExchanges: analysisRun.llmExchanges }
|
|
1586
|
+
: {}),
|
|
1587
|
+
...("evidence" in analysisRun
|
|
1588
|
+
? { evidence: analysisRun.evidence }
|
|
1589
|
+
: {}),
|
|
1590
|
+
...("boundaries" in analysisRun
|
|
1591
|
+
? { boundaries: analysisRun.boundaries }
|
|
1592
|
+
: {}),
|
|
1593
|
+
});
|
|
1594
|
+
const completedAnalysis = next.cells.find((candidate) => candidate.id === cell.id);
|
|
1595
|
+
if (!completedAnalysis?.outputArtifactId) {
|
|
1596
|
+
throw new Error("Analysis did not produce a script artifact");
|
|
1597
|
+
}
|
|
1598
|
+
return {
|
|
1599
|
+
artifactId: completedAnalysis.outputArtifactId,
|
|
1600
|
+
tool: cell.tool ?? OPENAI_ANALYSIS_TOOL,
|
|
1601
|
+
document: next,
|
|
1602
|
+
};
|
|
1603
|
+
}
|
|
1604
|
+
if (cell.type === "concat" && cell.operation === "concat_video") {
|
|
1605
|
+
const assembled = await assembleCanonicalSequence(engine, document, cell);
|
|
1606
|
+
const currentSequence = engine.sequences.getPrimary();
|
|
1607
|
+
const frameSeconds = currentSequence.frameRate.denominator
|
|
1608
|
+
/ currentSequence.frameRate.numerator;
|
|
1609
|
+
const captions = currentSequence.captions.map((caption) => ({
|
|
1610
|
+
text: caption.text,
|
|
1611
|
+
startSeconds: caption.timelineStartFrame * frameSeconds,
|
|
1612
|
+
durationSeconds: caption.durationFrames * frameSeconds,
|
|
1613
|
+
}));
|
|
1614
|
+
const result = await exportSequenceMaster(engine, assembled.clipArtifactIds, assembled.audioArtifactId, notebookGridTag(cell.slot), { captions });
|
|
1615
|
+
return {
|
|
1616
|
+
artifactId: result.artifactId,
|
|
1617
|
+
tool: "ffmpeg",
|
|
1618
|
+
cellInputs: {
|
|
1619
|
+
sequenceRevision: assembled.sequenceRevision,
|
|
1620
|
+
sequenceId: assembled.sequenceId,
|
|
1621
|
+
clipArtifactIds: assembled.clipArtifactIds,
|
|
1622
|
+
audioArtifactId: assembled.audioArtifactId,
|
|
1623
|
+
assemblyDurationFrames: assembled.assemblyDurationFrames,
|
|
1624
|
+
state: "completed",
|
|
1625
|
+
},
|
|
1626
|
+
};
|
|
1627
|
+
}
|
|
1628
|
+
if (cell.type === "splice"
|
|
1629
|
+
&& (cell.operation === "splice_video"
|
|
1630
|
+
|| cell.operation === "smooth_scene_concat")) {
|
|
1631
|
+
const result = await executeSpliceCell(engine, bookId, document, cell);
|
|
1632
|
+
return {
|
|
1633
|
+
artifactId: result.artifactId,
|
|
1634
|
+
tool: result.tool,
|
|
1635
|
+
cellInputs: {
|
|
1636
|
+
state: "completed",
|
|
1637
|
+
},
|
|
1638
|
+
};
|
|
1639
|
+
}
|
|
1640
|
+
if (cell.type === "prompt" && cell.operation === "rewrite_prompt") {
|
|
1641
|
+
if (!options.confirmPaid) {
|
|
1642
|
+
throw new Error(`Paid prompt rewrite for ${notebookGridTag(cell.slot)} requires confirmation before starting`);
|
|
1643
|
+
}
|
|
1644
|
+
const output = await rewritePromptCell(engine, document, cell, bookId);
|
|
1645
|
+
return {
|
|
1646
|
+
artifactId: output.artifactId,
|
|
1647
|
+
tool: output.tool,
|
|
1648
|
+
cellInputs: {
|
|
1649
|
+
resolvedPrompt: output.resolvedPrompt,
|
|
1650
|
+
rawRewriteResult: output.rawResult,
|
|
1651
|
+
state: "completed",
|
|
1652
|
+
},
|
|
1653
|
+
};
|
|
1654
|
+
}
|
|
1655
|
+
if ((cell.type === "generate_video"
|
|
1656
|
+
|| cell.type === "generate_image"
|
|
1657
|
+
|| cell.type === "generate_audio")
|
|
1658
|
+
&& (notebookCellModel(cell) !== null
|
|
1659
|
+
|| cell.tool === "edit_image_subjects")) {
|
|
1660
|
+
const requiresConfirmation = cell.inputs?.requiresConfirmation === true
|
|
1661
|
+
|| (cell.tool === "edit_image_subjects" && cell.provider !== "local")
|
|
1662
|
+
|| (cell.model ?? "").includes("seedance")
|
|
1663
|
+
|| cell.model === LIGHTNING_VIDEO_MODEL;
|
|
1664
|
+
if (requiresConfirmation && !options.confirmPaid) {
|
|
1665
|
+
throw new Error(`Paid generation for ${notebookGridTag(cell.slot)} requires confirmation before starting`);
|
|
1666
|
+
}
|
|
1667
|
+
const output = await generationTool(engine, document, cell, bookId, {
|
|
1668
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
1669
|
+
...(options.outputCellId ? { outputCellId: options.outputCellId } : {}),
|
|
1670
|
+
});
|
|
1671
|
+
const surface = cell.type === "generate_image"
|
|
1672
|
+
? "image_generate"
|
|
1673
|
+
: cell.type === "generate_video"
|
|
1674
|
+
? "video_generate"
|
|
1675
|
+
: cell.type === "generate_audio"
|
|
1676
|
+
? "audio_generate"
|
|
1677
|
+
: null;
|
|
1678
|
+
if (surface && output.resolvedPrompt) {
|
|
1679
|
+
void engine.prompts.record({
|
|
1680
|
+
surface,
|
|
1681
|
+
prompt: output.resolvedPrompt,
|
|
1682
|
+
context: {
|
|
1683
|
+
notebookId: document.id,
|
|
1684
|
+
cellId: cell.id,
|
|
1685
|
+
tool: output.tool,
|
|
1686
|
+
model: output.model,
|
|
1687
|
+
artifactId: output.artifactId,
|
|
1688
|
+
},
|
|
1689
|
+
}).catch(() => { });
|
|
1690
|
+
}
|
|
1691
|
+
return {
|
|
1692
|
+
artifactId: output.artifactId,
|
|
1693
|
+
tool: output.tool,
|
|
1694
|
+
provider: output.provider,
|
|
1695
|
+
model: output.model,
|
|
1696
|
+
awaitingOutput: true,
|
|
1697
|
+
cellInputs: {
|
|
1698
|
+
providerArtifactId: output.artifactId,
|
|
1699
|
+
...(output.resolvedPrompt
|
|
1700
|
+
? { resolvedPrompt: output.resolvedPrompt }
|
|
1701
|
+
: {}),
|
|
1702
|
+
state: "awaiting-output",
|
|
1703
|
+
},
|
|
1704
|
+
};
|
|
1705
|
+
}
|
|
1706
|
+
if (cell.type === "generate_video" || cell.type === "generate_image" || cell.type === "generate_audio") {
|
|
1707
|
+
throw new Error(`${notebookGridTag(cell.slot)} uses ${cell.model || cell.tool || "no model"}, which is not a generation model; pick one from list_generation_models`);
|
|
1708
|
+
}
|
|
1709
|
+
throw new Error(`Cell operation ${cell.operation ?? "unset"} is not runnable`);
|
|
1710
|
+
}
|
|
1711
|
+
export async function runNotebookCell(engine, bookId, notebookId, document, cell, options = {}) {
|
|
1712
|
+
let normalizedDocument = migrateStackedGenerationResults(engine, normalizeLegacyExtractCells(normalizeLegacyAnalyzeDocument(document)));
|
|
1713
|
+
let normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizeLegacyAnalyzeCell(cell);
|
|
1714
|
+
const generateMediaOutput = normalizedCell.type === "generate_audio"
|
|
1715
|
+
? ensureGenerationMediaOutput(engine, normalizedDocument, normalizedCell)
|
|
1716
|
+
: isConcurrentGenerationProducer(normalizedCell)
|
|
1717
|
+
? findGenerationResultForRun(normalizedDocument, normalizedCell, {
|
|
1718
|
+
runId: options.runId,
|
|
1719
|
+
outputCellId: options.outputCellId,
|
|
1720
|
+
})
|
|
1721
|
+
: null;
|
|
1722
|
+
if (isConcurrentGenerationProducer(normalizedCell)
|
|
1723
|
+
&& !generateMediaOutput) {
|
|
1724
|
+
if (options.outputCellId) {
|
|
1725
|
+
throw new Error(`Generation output cell not found for run: ${options.outputCellId}`);
|
|
1726
|
+
}
|
|
1727
|
+
const allocated = normalizedCell.type === "generate_image"
|
|
1728
|
+
? allocateGenerateImageResult(engine, normalizedDocument, normalizedCell.id, {
|
|
1729
|
+
runId: options.runId ?? uuidv7(),
|
|
1730
|
+
acceptedAt: new Date().toISOString(),
|
|
1731
|
+
})
|
|
1732
|
+
: allocateGenerateVideoResult(engine, normalizedDocument, normalizedCell.id, {
|
|
1733
|
+
runId: options.runId ?? uuidv7(),
|
|
1734
|
+
acceptedAt: new Date().toISOString(),
|
|
1735
|
+
});
|
|
1736
|
+
const reserved = await mutateNotebook(engine, notebookId, normalizedCell.type === "generate_image"
|
|
1737
|
+
? "allocate_generate_image_output"
|
|
1738
|
+
: "allocate_generate_video_output", () => allocated.document, { cellId: normalizedCell.id, outputCellId: allocated.outputCell.id });
|
|
1739
|
+
if (!reserved.ok)
|
|
1740
|
+
throw new Error(reserved.error.message);
|
|
1741
|
+
const refreshed = engine.notebooks.read(notebookId);
|
|
1742
|
+
if (!refreshed.ok)
|
|
1743
|
+
throw new Error(refreshed.error.message);
|
|
1744
|
+
normalizedDocument = refreshed.value;
|
|
1745
|
+
normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizedCell;
|
|
1746
|
+
options = { ...options, outputCellId: allocated.outputCell.id };
|
|
1747
|
+
}
|
|
1748
|
+
if (generateMediaOutput
|
|
1749
|
+
&& "document" in generateMediaOutput
|
|
1750
|
+
&& generateMediaOutput.document !== normalizedDocument) {
|
|
1751
|
+
const repaired = await mutateNotebook(engine, notebookId, "ensure_generate_audio_output", (current) => ensureGenerationMediaOutput(engine, current, normalizedCell).document, { cellId: normalizedCell.id });
|
|
1752
|
+
if (!repaired.ok)
|
|
1753
|
+
throw new Error(repaired.error.message);
|
|
1754
|
+
const refreshed = engine.notebooks.read(notebookId);
|
|
1755
|
+
if (!refreshed.ok)
|
|
1756
|
+
throw new Error(refreshed.error.message);
|
|
1757
|
+
normalizedDocument = normalizeLegacyAnalyzeDocument(refreshed.value);
|
|
1758
|
+
normalizedCell = normalizedDocument.cells.find((candidate) => candidate.id === cell.id) ?? normalizedCell;
|
|
1759
|
+
}
|
|
1760
|
+
const nextFingerprint = notebookCellFingerprint(normalizedDocument, normalizedCell);
|
|
1761
|
+
const startedAt = new Date().toISOString();
|
|
1762
|
+
const runningState = {
|
|
1763
|
+
fingerprint: nextFingerprint,
|
|
1764
|
+
status: "running",
|
|
1765
|
+
...(normalizedCell.outputArtifactId
|
|
1766
|
+
? { outputArtifactId: normalizedCell.outputArtifactId }
|
|
1767
|
+
: {}),
|
|
1768
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
1769
|
+
startedAt,
|
|
1770
|
+
updatedAt: startedAt,
|
|
1771
|
+
tool: normalizedCell.tool
|
|
1772
|
+
|| normalizedCell.model
|
|
1773
|
+
|| normalizedCell.operation,
|
|
1774
|
+
};
|
|
1775
|
+
const started = await mutateNotebook(engine, notebookId, "start_notebook_cell", (current) => withExecution(healGenerationMediaOutputResidue(normalizeLegacyAnalyzeDocument(current), normalizedCell, options.outputCellId), options.outputCellId && isConcurrentGenerationProducer(normalizedCell)
|
|
1776
|
+
? options.outputCellId
|
|
1777
|
+
: normalizedCell.id, runningState), { cellId: normalizedCell.id, runId: options.runId, outputCellId: options.outputCellId });
|
|
1778
|
+
if (!started.ok)
|
|
1779
|
+
throw new Error(started.error.message);
|
|
1780
|
+
const activeDocument = withExecution(healGenerationMediaOutputResidue(normalizedDocument, normalizedCell, options.outputCellId), options.outputCellId && isConcurrentGenerationProducer(normalizedCell)
|
|
1781
|
+
? options.outputCellId
|
|
1782
|
+
: normalizedCell.id, runningState);
|
|
1783
|
+
const activeCell = activeDocument.cells.find((candidate) => candidate.id === normalizedCell.id);
|
|
1784
|
+
if (!activeCell) {
|
|
1785
|
+
throw new Error(`Notebook cell not found: ${normalizedCell.id}`);
|
|
1786
|
+
}
|
|
1787
|
+
try {
|
|
1788
|
+
const output = await executeLocalOrGeneration(engine, bookId, activeDocument, activeCell, options);
|
|
1789
|
+
const updatedAt = new Date().toISOString();
|
|
1790
|
+
const status = output.awaitingOutput ? "awaiting-output" : "completed";
|
|
1791
|
+
const state = {
|
|
1792
|
+
fingerprint: nextFingerprint,
|
|
1793
|
+
status,
|
|
1794
|
+
outputArtifactId: output.artifactId,
|
|
1795
|
+
...(output.awaitingOutput
|
|
1796
|
+
? { providerArtifactId: output.artifactId }
|
|
1797
|
+
: { completedAt: updatedAt }),
|
|
1798
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
1799
|
+
updatedAt,
|
|
1800
|
+
tool: output.tool,
|
|
1801
|
+
};
|
|
1802
|
+
const write = await mutateNotebook(engine, notebookId, "run_notebook_cell", (current) => {
|
|
1803
|
+
const base = output.document ?? current;
|
|
1804
|
+
const mediaOutput = findGenerationResultForRun(base, activeCell, {
|
|
1805
|
+
runId: options.runId,
|
|
1806
|
+
outputCellId: options.outputCellId,
|
|
1807
|
+
});
|
|
1808
|
+
// New-style generate_video / generate_image: artifact lives on the
|
|
1809
|
+
// dedicated output cell. Legacy/other cells keep inline
|
|
1810
|
+
// outputArtifactId on the action cell.
|
|
1811
|
+
const actionArtifactId = mediaOutput ? undefined : output.artifactId;
|
|
1812
|
+
const executionCellId = options.outputCellId
|
|
1813
|
+
&& isConcurrentGenerationProducer(activeCell)
|
|
1814
|
+
? options.outputCellId
|
|
1815
|
+
: activeCell.id;
|
|
1816
|
+
let withOutput = withExecution(base, executionCellId, state, actionArtifactId);
|
|
1817
|
+
const resolvedPrompt = typeof output.cellInputs?.resolvedPrompt === "string"
|
|
1818
|
+
? output.cellInputs.resolvedPrompt
|
|
1819
|
+
: "";
|
|
1820
|
+
if (mediaOutput) {
|
|
1821
|
+
withOutput = withMediaOutput(withOutput, new Set([mediaOutput.outputCell.id]), output.artifactId, {
|
|
1822
|
+
state: status,
|
|
1823
|
+
...(resolvedPrompt ? { resolvedPrompt } : {}),
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
const actionBefore = withOutput.cells.find((candidate) => candidate.id === activeCell.id);
|
|
1827
|
+
const fillActionPrompt = Boolean(resolvedPrompt)
|
|
1828
|
+
&& !actionBefore?.prompt;
|
|
1829
|
+
const fillActionModel = output.model !== undefined
|
|
1830
|
+
&& actionBefore?.tool !== "edit_image_subjects"
|
|
1831
|
+
&& output.model !== actionBefore?.model;
|
|
1832
|
+
const fillActionTool = fillActionModel && !actionBefore?.tool;
|
|
1833
|
+
const actionCellChanged = fillActionPrompt || fillActionModel;
|
|
1834
|
+
const concurrentProducer = isConcurrentGenerationProducer(activeCell);
|
|
1835
|
+
const withCells = {
|
|
1836
|
+
...withOutput,
|
|
1837
|
+
cells: withOutput.cells.map((candidate) => {
|
|
1838
|
+
if (candidate.id === activeCell.id) {
|
|
1839
|
+
const nextInputs = { ...(candidate.inputs ?? {}) };
|
|
1840
|
+
if (!concurrentProducer && output.cellInputs) {
|
|
1841
|
+
Object.assign(nextInputs, output.cellInputs);
|
|
1842
|
+
}
|
|
1843
|
+
else if (concurrentProducer) {
|
|
1844
|
+
delete nextInputs.state;
|
|
1845
|
+
delete nextInputs.providerArtifactId;
|
|
1846
|
+
if (typeof output.cellInputs?.resolvedPrompt === "string"
|
|
1847
|
+
&& output.cellInputs.resolvedPrompt) {
|
|
1848
|
+
nextInputs.resolvedPrompt = output.cellInputs.resolvedPrompt;
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return {
|
|
1852
|
+
...candidate,
|
|
1853
|
+
...(fillActionPrompt ? { prompt: resolvedPrompt } : {}),
|
|
1854
|
+
...(fillActionModel ? { model: output.model } : {}),
|
|
1855
|
+
...(fillActionTool ? { tool: output.tool } : {}),
|
|
1856
|
+
inputs: nextInputs,
|
|
1857
|
+
};
|
|
1858
|
+
}
|
|
1859
|
+
if (candidate.id === mediaOutput?.outputCell.id) {
|
|
1860
|
+
if (!resolvedPrompt && output.model === undefined) {
|
|
1861
|
+
return candidate;
|
|
1862
|
+
}
|
|
1863
|
+
return {
|
|
1864
|
+
...candidate,
|
|
1865
|
+
...(resolvedPrompt ? { prompt: resolvedPrompt } : {}),
|
|
1866
|
+
...(output.model !== undefined ? { model: output.model } : {}),
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
return candidate;
|
|
1870
|
+
}),
|
|
1871
|
+
};
|
|
1872
|
+
// Filling the empty prompt column changes the cell's fingerprint, so
|
|
1873
|
+
// the stored execution fingerprint must come from the updated
|
|
1874
|
+
// document — otherwise the cell would immediately read as stale.
|
|
1875
|
+
const finalCell = withCells.cells.find((candidate) => candidate.id === activeCell.id);
|
|
1876
|
+
const fingerprint = actionCellChanged && finalCell
|
|
1877
|
+
? notebookCellFingerprint(withCells, finalCell)
|
|
1878
|
+
: state.fingerprint;
|
|
1879
|
+
if (!mediaOutput && !actionCellChanged)
|
|
1880
|
+
return withCells;
|
|
1881
|
+
const concurrent = isConcurrentGenerationProducer(activeCell);
|
|
1882
|
+
const nextExecution = { ...executionState(withCells) };
|
|
1883
|
+
if (concurrent
|
|
1884
|
+
&& options.runId
|
|
1885
|
+
&& nextExecution[activeCell.id]?.runId === options.runId) {
|
|
1886
|
+
delete nextExecution[activeCell.id];
|
|
1887
|
+
}
|
|
1888
|
+
return {
|
|
1889
|
+
...withCells,
|
|
1890
|
+
execution: {
|
|
1891
|
+
...nextExecution,
|
|
1892
|
+
...(concurrent
|
|
1893
|
+
? {}
|
|
1894
|
+
: {
|
|
1895
|
+
[activeCell.id]: {
|
|
1896
|
+
...state,
|
|
1897
|
+
fingerprint,
|
|
1898
|
+
outputArtifactId: output.artifactId,
|
|
1899
|
+
},
|
|
1900
|
+
}),
|
|
1901
|
+
...(mediaOutput
|
|
1902
|
+
? {
|
|
1903
|
+
[mediaOutput.outputCell.id]: {
|
|
1904
|
+
fingerprint,
|
|
1905
|
+
status: status === "awaiting-output"
|
|
1906
|
+
? "awaiting-output"
|
|
1907
|
+
: "completed",
|
|
1908
|
+
outputArtifactId: output.artifactId,
|
|
1909
|
+
...(state.providerArtifactId
|
|
1910
|
+
? { providerArtifactId: state.providerArtifactId }
|
|
1911
|
+
: {}),
|
|
1912
|
+
updatedAt: state.updatedAt,
|
|
1913
|
+
...(state.completedAt
|
|
1914
|
+
? { completedAt: state.completedAt }
|
|
1915
|
+
: {}),
|
|
1916
|
+
...(state.runId ? { runId: state.runId } : {}),
|
|
1917
|
+
tool: state.tool,
|
|
1918
|
+
},
|
|
1919
|
+
}
|
|
1920
|
+
: {}),
|
|
1921
|
+
},
|
|
1922
|
+
};
|
|
1923
|
+
}, {
|
|
1924
|
+
cellId: activeCell.id,
|
|
1925
|
+
outputArtifactId: output.artifactId,
|
|
1926
|
+
tool: output.tool,
|
|
1927
|
+
status,
|
|
1928
|
+
runId: options.runId,
|
|
1929
|
+
}, { artifactId: output.artifactId });
|
|
1930
|
+
if (!write.ok)
|
|
1931
|
+
throw new Error(write.error.message);
|
|
1932
|
+
const read = engine.notebooks.read(notebookId);
|
|
1933
|
+
if (!read.ok)
|
|
1934
|
+
throw new Error(read.error.message);
|
|
1935
|
+
return {
|
|
1936
|
+
document: read.value,
|
|
1937
|
+
outputArtifactId: output.artifactId,
|
|
1938
|
+
status,
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
catch (cause) {
|
|
1942
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
1943
|
+
await patchGeneration(engine, activeGenerationIdForRun(engine, notebookId, activeCell.id, options.runId, undefined), { status: "failed", error: truncateGenerationError(message) });
|
|
1944
|
+
const outputArtifactId = await failedArtifact(engine, activeCell, message);
|
|
1945
|
+
const state = {
|
|
1946
|
+
fingerprint: nextFingerprint,
|
|
1947
|
+
status: "error",
|
|
1948
|
+
outputArtifactId,
|
|
1949
|
+
completedAt: new Date().toISOString(),
|
|
1950
|
+
updatedAt: new Date().toISOString(),
|
|
1951
|
+
...(options.runId ? { runId: options.runId } : {}),
|
|
1952
|
+
tool: activeCell.tool
|
|
1953
|
+
|| activeCell.model
|
|
1954
|
+
|| activeCell.operation,
|
|
1955
|
+
error: message,
|
|
1956
|
+
};
|
|
1957
|
+
const write = await mutateNotebook(engine, notebookId, "run_notebook_cell_failed", (current) => {
|
|
1958
|
+
const concurrent = isConcurrentGenerationProducer(activeCell);
|
|
1959
|
+
const targetCellId = concurrent && options.outputCellId
|
|
1960
|
+
? options.outputCellId
|
|
1961
|
+
: activeCell.id;
|
|
1962
|
+
const withFailure = withExecution(current, targetCellId, state, concurrent ? undefined : outputArtifactId);
|
|
1963
|
+
if (!concurrent)
|
|
1964
|
+
return withFailure;
|
|
1965
|
+
const execution = { ...executionState(withFailure) };
|
|
1966
|
+
if (options.runId
|
|
1967
|
+
&& execution[activeCell.id]?.runId === options.runId) {
|
|
1968
|
+
delete execution[activeCell.id];
|
|
1969
|
+
}
|
|
1970
|
+
const cells = withFailure.cells.map((candidate) => {
|
|
1971
|
+
if (candidate.id !== activeCell.id)
|
|
1972
|
+
return candidate;
|
|
1973
|
+
const inputs = { ...(candidate.inputs ?? {}) };
|
|
1974
|
+
delete inputs.state;
|
|
1975
|
+
delete inputs.providerArtifactId;
|
|
1976
|
+
return { ...candidate, inputs };
|
|
1977
|
+
});
|
|
1978
|
+
return { ...withFailure, execution, cells };
|
|
1979
|
+
}, {
|
|
1980
|
+
cellId: activeCell.id,
|
|
1981
|
+
outputArtifactId,
|
|
1982
|
+
error: message.slice(0, 500),
|
|
1983
|
+
runId: options.runId,
|
|
1984
|
+
tool: state.tool,
|
|
1985
|
+
}, { artifactId: outputArtifactId });
|
|
1986
|
+
if (!write.ok)
|
|
1987
|
+
throw new Error(write.error.message);
|
|
1988
|
+
throw new Error(message);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
export function notebookCellIsStale(engine, document, cell) {
|
|
1992
|
+
const execution = executionState(document)[cell.id];
|
|
1993
|
+
if (!execution)
|
|
1994
|
+
return true;
|
|
1995
|
+
if (execution.status === "running"
|
|
1996
|
+
|| execution.status === "awaiting-output") {
|
|
1997
|
+
return false;
|
|
1998
|
+
}
|
|
1999
|
+
if (execution.fingerprint !== notebookCellFingerprint(document, cell))
|
|
2000
|
+
return true;
|
|
2001
|
+
if (execution.status === "error" || execution.error)
|
|
2002
|
+
return true;
|
|
2003
|
+
if (execution.status === "stale" || execution.stale)
|
|
2004
|
+
return true;
|
|
2005
|
+
// generate_video / generate_image may hold the artifact only on the output cell.
|
|
2006
|
+
const artifactId = resolveNotebookCellArtifactId(document, cell)
|
|
2007
|
+
?? execution.outputArtifactId;
|
|
2008
|
+
if (!artifactId)
|
|
2009
|
+
return true;
|
|
2010
|
+
if (!engine.artifacts.get(artifactId).ok)
|
|
2011
|
+
return true;
|
|
2012
|
+
return false;
|
|
2013
|
+
}
|
|
2014
|
+
function notebookCellNeedsExecution(engine, document, cell) {
|
|
2015
|
+
if (notebookCellIsRunnable(cell)) {
|
|
2016
|
+
return notebookCellIsStale(engine, document, cell);
|
|
2017
|
+
}
|
|
2018
|
+
const artifactId = resolveNotebookCellArtifactId(document, cell)
|
|
2019
|
+
?? cell.outputArtifactId
|
|
2020
|
+
?? cell.outputEntityId;
|
|
2021
|
+
return !artifactId || !engine.artifacts.get(artifactId).ok;
|
|
2022
|
+
}
|
|
2023
|
+
function notebookCellHasReusableMediaOutput(engine, document, cell) {
|
|
2024
|
+
const mediaType = notebookCellMediaType(cell);
|
|
2025
|
+
if (!mediaType)
|
|
2026
|
+
return false;
|
|
2027
|
+
const artifactId = resolveNotebookCellArtifactId(document, cell)
|
|
2028
|
+
?? cell.outputArtifactId
|
|
2029
|
+
?? cell.outputEntityId;
|
|
2030
|
+
if (!artifactId)
|
|
2031
|
+
return false;
|
|
2032
|
+
const artifact = engine.artifacts.get(artifactId);
|
|
2033
|
+
const artifactMediaType = artifact.ok && artifact.value.kind === "final"
|
|
2034
|
+
? "video"
|
|
2035
|
+
: artifact.ok ? artifact.value.kind : null;
|
|
2036
|
+
if (!artifact.ok || artifactMediaType !== mediaType)
|
|
2037
|
+
return false;
|
|
2038
|
+
if (!notebookCellIsRunnable(cell))
|
|
2039
|
+
return true;
|
|
2040
|
+
const mediaOutput = findGenerationMediaOutput(document, cell);
|
|
2041
|
+
if (mediaOutput) {
|
|
2042
|
+
const outputExecution = executionState(document)[mediaOutput.outputCell.id];
|
|
2043
|
+
if (mediaOutput.outputCell.inputs?.state === "completed"
|
|
2044
|
+
|| outputExecution?.status === "completed") {
|
|
2045
|
+
return true;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
const execution = executionState(document)[cell.id];
|
|
2049
|
+
return execution?.status === "completed" && !execution.error;
|
|
2050
|
+
}
|
|
2051
|
+
function requiredNotebookCellIds(engine, document, cellIds, forceSelected) {
|
|
2052
|
+
const cells = new Map(document.cells.map((cell) => [cell.id, cell]));
|
|
2053
|
+
const parents = new Map();
|
|
2054
|
+
for (const edge of document.edges) {
|
|
2055
|
+
const list = parents.get(edge.target) ?? [];
|
|
2056
|
+
list.push(edge.source);
|
|
2057
|
+
parents.set(edge.target, list);
|
|
2058
|
+
}
|
|
2059
|
+
const required = new Set(cellIds);
|
|
2060
|
+
const expanded = new Set();
|
|
2061
|
+
const stack = cellIds.filter((cellId) => {
|
|
2062
|
+
const cell = cells.get(cellId);
|
|
2063
|
+
return forceSelected || !cell || notebookCellNeedsExecution(engine, document, cell);
|
|
2064
|
+
});
|
|
2065
|
+
while (stack.length > 0) {
|
|
2066
|
+
const id = stack.pop();
|
|
2067
|
+
if (!id || expanded.has(id))
|
|
2068
|
+
continue;
|
|
2069
|
+
expanded.add(id);
|
|
2070
|
+
for (const parentId of parents.get(id) ?? []) {
|
|
2071
|
+
const parent = cells.get(parentId);
|
|
2072
|
+
if (parent
|
|
2073
|
+
&& notebookCellHasReusableMediaOutput(engine, document, parent)) {
|
|
2074
|
+
continue;
|
|
2075
|
+
}
|
|
2076
|
+
required.add(parentId);
|
|
2077
|
+
if (!parent)
|
|
2078
|
+
continue;
|
|
2079
|
+
if (!notebookCellNeedsExecution(engine, document, parent))
|
|
2080
|
+
continue;
|
|
2081
|
+
stack.push(parentId);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
return required;
|
|
2085
|
+
}
|
|
2086
|
+
function generationPlanClaimField(plan, field) {
|
|
2087
|
+
const claim = plan.executionClaim;
|
|
2088
|
+
return claim
|
|
2089
|
+
&& typeof claim === "object"
|
|
2090
|
+
&& !Array.isArray(claim)
|
|
2091
|
+
&& typeof claim[field] === "string"
|
|
2092
|
+
? claim[field]
|
|
2093
|
+
: null;
|
|
2094
|
+
}
|
|
2095
|
+
function generationPlanClaimRunId(plan) {
|
|
2096
|
+
return generationPlanClaimField(plan, "runId");
|
|
2097
|
+
}
|
|
2098
|
+
export function generationPlanClaimOutputCellId(plan) {
|
|
2099
|
+
return generationPlanClaimField(plan, "outputCellId");
|
|
2100
|
+
}
|
|
2101
|
+
function concurrentProducerIdForCell(cell) {
|
|
2102
|
+
if (isConcurrentGenerationProducer(cell))
|
|
2103
|
+
return cell.id;
|
|
2104
|
+
const producerId = cell.inputs?.producerCellId;
|
|
2105
|
+
if (isImageVideoGenerationResult(cell) && typeof producerId === "string") {
|
|
2106
|
+
return producerId;
|
|
2107
|
+
}
|
|
2108
|
+
return null;
|
|
2109
|
+
}
|
|
2110
|
+
function awaitJobCoverageIds(payload) {
|
|
2111
|
+
const ids = [];
|
|
2112
|
+
if (typeof payload.cellId === "string")
|
|
2113
|
+
ids.push(payload.cellId);
|
|
2114
|
+
if (typeof payload.outputCellId === "string")
|
|
2115
|
+
ids.push(payload.outputCellId);
|
|
2116
|
+
return [...new Set(ids)];
|
|
2117
|
+
}
|
|
2118
|
+
function awaitJobCoversExecution(input) {
|
|
2119
|
+
if (input.payload.notebookId !== input.notebookId)
|
|
2120
|
+
return false;
|
|
2121
|
+
if (typeof input.payload.runId === "string"
|
|
2122
|
+
&& input.payload.runId !== input.runId) {
|
|
2123
|
+
return false;
|
|
2124
|
+
}
|
|
2125
|
+
const covered = awaitJobCoverageIds(input.payload);
|
|
2126
|
+
if (covered.includes(input.cellId))
|
|
2127
|
+
return true;
|
|
2128
|
+
if (input.producerCellId && covered.includes(input.producerCellId)) {
|
|
2129
|
+
const jobOutput = typeof input.payload.outputCellId === "string"
|
|
2130
|
+
? input.payload.outputCellId
|
|
2131
|
+
: null;
|
|
2132
|
+
if (!jobOutput)
|
|
2133
|
+
return true;
|
|
2134
|
+
return jobOutput === input.cellId;
|
|
2135
|
+
}
|
|
2136
|
+
return false;
|
|
2137
|
+
}
|
|
2138
|
+
function resolveAwaitNotebookTarget(document, input) {
|
|
2139
|
+
const requested = document.cells.find((candidate) => candidate.id === input.cellId);
|
|
2140
|
+
if (!requested) {
|
|
2141
|
+
return { cell: undefined, outputCellId: input.outputCellId };
|
|
2142
|
+
}
|
|
2143
|
+
if (isImageVideoGenerationResult(requested)) {
|
|
2144
|
+
const producerId = requested.inputs?.producerCellId;
|
|
2145
|
+
const producer = typeof producerId === "string"
|
|
2146
|
+
? document.cells.find((candidate) => candidate.id === producerId)
|
|
2147
|
+
: undefined;
|
|
2148
|
+
return {
|
|
2149
|
+
cell: producer ?? requested,
|
|
2150
|
+
outputCellId: input.outputCellId ?? requested.id,
|
|
2151
|
+
};
|
|
2152
|
+
}
|
|
2153
|
+
return { cell: requested, outputCellId: input.outputCellId };
|
|
2154
|
+
}
|
|
2155
|
+
export async function claimNotebookGenerationPlan(engine, notebookId, cellId, planId, runId, retryFailedCellId) {
|
|
2156
|
+
let claimedPlan = null;
|
|
2157
|
+
const claimedAt = new Date().toISOString();
|
|
2158
|
+
const result = await mutateNotebook(engine, notebookId, "claim_notebook_generation_plan", (document) => {
|
|
2159
|
+
const record = (document.generationPlans ?? []).find((candidate) => candidate.planId === planId && candidate.cellId === cellId);
|
|
2160
|
+
if (!record)
|
|
2161
|
+
throw new Error("Generation plan is not preserved in this notebook");
|
|
2162
|
+
if (record.status !== "planned") {
|
|
2163
|
+
throw new Error(`Generation plan is already ${record.status}`);
|
|
2164
|
+
}
|
|
2165
|
+
if (generationPlanClaimRunId(record.plan)) {
|
|
2166
|
+
throw new Error("Generation plan is already claimed");
|
|
2167
|
+
}
|
|
2168
|
+
const cell = document.cells.find((candidate) => candidate.id === cellId);
|
|
2169
|
+
if (!cell)
|
|
2170
|
+
throw new Error("Notebook cell not found");
|
|
2171
|
+
const expected = record.plan.cellDefinitionFingerprint
|
|
2172
|
+
?? record.plan.cellFingerprint;
|
|
2173
|
+
if (expected !== notebookCellDefinitionFingerprint(document, cell)) {
|
|
2174
|
+
throw new Error("Generation plan is stale for the selected cell");
|
|
2175
|
+
}
|
|
2176
|
+
const execution = executionState(document)[cellId];
|
|
2177
|
+
const concurrent = isConcurrentGenerationProducer(cell);
|
|
2178
|
+
if (!concurrent
|
|
2179
|
+
&& (execution?.status === "queued"
|
|
2180
|
+
|| execution?.status === "running"
|
|
2181
|
+
|| execution?.status === "awaiting-output")) {
|
|
2182
|
+
throw new Error(`Notebook cell ${notebookGridTag(cell.slot)} is already ${execution.status}`);
|
|
2183
|
+
}
|
|
2184
|
+
let working = retryFailedCellId
|
|
2185
|
+
? removeCreditFailedOutput(engine, document, cellId, retryFailedCellId)
|
|
2186
|
+
: document;
|
|
2187
|
+
let outputCellId;
|
|
2188
|
+
if (cell.type === "generate_image" || cell.type === "generate_video"
|
|
2189
|
+
|| (cell.type === "generate_audio" && retryFailedCellId)) {
|
|
2190
|
+
const allocate = cell.type === "generate_image" ? allocateGenerateImageResult
|
|
2191
|
+
: cell.type === "generate_audio" ? allocateGenerateAudioResult
|
|
2192
|
+
: allocateGenerateVideoResult;
|
|
2193
|
+
const allocated = allocate(engine, working, cell.id, { runId, planId, acceptedAt: claimedAt });
|
|
2194
|
+
working = allocated.document;
|
|
2195
|
+
outputCellId = allocated.outputCell.id;
|
|
2196
|
+
const retrySlot = document.cells.find((candidate) => candidate.id === retryFailedCellId)?.slot;
|
|
2197
|
+
if (retrySlot) {
|
|
2198
|
+
working = {
|
|
2199
|
+
...working,
|
|
2200
|
+
cells: working.cells.map((candidate) => candidate.id === outputCellId
|
|
2201
|
+
? { ...candidate, slot: retrySlot }
|
|
2202
|
+
: candidate),
|
|
2203
|
+
};
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
claimedPlan = {
|
|
2207
|
+
...record.plan,
|
|
2208
|
+
executionClaim: {
|
|
2209
|
+
runId,
|
|
2210
|
+
claimedAt,
|
|
2211
|
+
...(outputCellId ? { outputCellId } : {}),
|
|
2212
|
+
},
|
|
2213
|
+
};
|
|
2214
|
+
const state = {
|
|
2215
|
+
fingerprint: notebookCellFingerprint(working, cell),
|
|
2216
|
+
status: "queued",
|
|
2217
|
+
runId,
|
|
2218
|
+
startedAt: claimedAt,
|
|
2219
|
+
updatedAt: claimedAt,
|
|
2220
|
+
tool: cell.tool || cell.model || cell.operation,
|
|
2221
|
+
};
|
|
2222
|
+
let queued = outputCellId
|
|
2223
|
+
? withExecution(working, outputCellId, state)
|
|
2224
|
+
: withExecution(working, cellId, {
|
|
2225
|
+
...state,
|
|
2226
|
+
...(cell.outputArtifactId
|
|
2227
|
+
? { outputArtifactId: cell.outputArtifactId }
|
|
2228
|
+
: {}),
|
|
2229
|
+
});
|
|
2230
|
+
if (outputCellId && !concurrent)
|
|
2231
|
+
queued = withExecution(queued, cellId, state);
|
|
2232
|
+
return {
|
|
2233
|
+
...queued,
|
|
2234
|
+
generationPlans: (queued.generationPlans ?? []).map((candidate) => candidate.planId === planId
|
|
2235
|
+
? {
|
|
2236
|
+
...candidate,
|
|
2237
|
+
status: "queued",
|
|
2238
|
+
plan: claimedPlan,
|
|
2239
|
+
updatedAt: claimedAt,
|
|
2240
|
+
}
|
|
2241
|
+
: candidate),
|
|
2242
|
+
};
|
|
2243
|
+
}, { notebookId, cellId, planId, runId });
|
|
2244
|
+
if (!result.ok)
|
|
2245
|
+
throw new Error(result.error.message);
|
|
2246
|
+
if (!claimedPlan)
|
|
2247
|
+
throw new Error("Generation plan claim was not persisted");
|
|
2248
|
+
return claimedPlan;
|
|
2249
|
+
}
|
|
2250
|
+
export async function failQueuedNotebookCells(engine, notebookId, options) {
|
|
2251
|
+
const selected = new Set(options.cellIds);
|
|
2252
|
+
if (selected.size === 0
|
|
2253
|
+
&& !options.generationPlanId
|
|
2254
|
+
&& !options.runPlanId)
|
|
2255
|
+
return 0;
|
|
2256
|
+
const snapshot = engine.notebooks.read(notebookId);
|
|
2257
|
+
if (!snapshot.ok)
|
|
2258
|
+
return 0;
|
|
2259
|
+
const queuedCellIds = new Set([
|
|
2260
|
+
...[...selected].filter((cellId) => {
|
|
2261
|
+
const state = executionState(snapshot.value)[cellId];
|
|
2262
|
+
return state?.status === "queued" && state.runId === options.runId;
|
|
2263
|
+
}),
|
|
2264
|
+
...Object.entries(executionState(snapshot.value)).flatMap(([cellId, state]) => state?.status === "queued" && state.runId === options.runId ? [cellId] : []),
|
|
2265
|
+
]);
|
|
2266
|
+
const failedCellIds = new Set([...selected].filter((cellId) => {
|
|
2267
|
+
const state = executionState(snapshot.value)[cellId];
|
|
2268
|
+
return state?.status === "error" && state.runId === options.runId;
|
|
2269
|
+
}));
|
|
2270
|
+
const terminalCellIds = new Set([...queuedCellIds, ...failedCellIds]);
|
|
2271
|
+
const hasGenerationPlan = (snapshot.value.generationPlans ?? []).some((record) => record.status === "queued"
|
|
2272
|
+
&& terminalCellIds.has(record.cellId)
|
|
2273
|
+
&& record.planId === options.generationPlanId
|
|
2274
|
+
&& (generationPlanClaimRunId(record.plan) === options.runId
|
|
2275
|
+
|| (options.allowLegacyUnboundGenerationPlan === true
|
|
2276
|
+
&& generationPlanClaimRunId(record.plan) === null)));
|
|
2277
|
+
const hasRunPlan = Boolean(options.runPlanId
|
|
2278
|
+
&& notebookRunPlanRecords(snapshot.value).some((record) => record.planId === options.runPlanId
|
|
2279
|
+
&& record.status === "queued"
|
|
2280
|
+
&& record.runId === options.runId
|
|
2281
|
+
&& (selected.size === 0 || terminalCellIds.size > 0)));
|
|
2282
|
+
if (queuedCellIds.size === 0 && !hasGenerationPlan && !hasRunPlan)
|
|
2283
|
+
return 0;
|
|
2284
|
+
const updatedAt = new Date().toISOString();
|
|
2285
|
+
let terminalized = 0;
|
|
2286
|
+
const result = await mutateNotebook(engine, notebookId, "fail_queued_notebook_cells", (document) => {
|
|
2287
|
+
const execution = { ...executionState(document) };
|
|
2288
|
+
const transitionedCellIds = new Set();
|
|
2289
|
+
for (const [cellId, state] of Object.entries(execution)) {
|
|
2290
|
+
if (state?.status !== "queued" || state.runId !== options.runId)
|
|
2291
|
+
continue;
|
|
2292
|
+
if (selected.size > 0 && !queuedCellIds.has(cellId) && !selected.has(cellId)) {
|
|
2293
|
+
continue;
|
|
2294
|
+
}
|
|
2295
|
+
execution[cellId] = {
|
|
2296
|
+
...state,
|
|
2297
|
+
status: "error",
|
|
2298
|
+
completedAt: updatedAt,
|
|
2299
|
+
updatedAt,
|
|
2300
|
+
error: options.error,
|
|
2301
|
+
};
|
|
2302
|
+
transitionedCellIds.add(cellId);
|
|
2303
|
+
}
|
|
2304
|
+
const planCellIds = new Set([...selected, ...transitionedCellIds]);
|
|
2305
|
+
for (const [cellId, state] of Object.entries(execution)) {
|
|
2306
|
+
if (state?.status === "error" && state.runId === options.runId) {
|
|
2307
|
+
planCellIds.add(cellId);
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
terminalized = transitionedCellIds.size;
|
|
2311
|
+
return {
|
|
2312
|
+
...document,
|
|
2313
|
+
execution,
|
|
2314
|
+
cells: document.cells.map((cell) => transitionedCellIds.has(cell.id)
|
|
2315
|
+
? {
|
|
2316
|
+
...cell,
|
|
2317
|
+
inputs: { ...(cell.inputs ?? {}), state: "error" },
|
|
2318
|
+
}
|
|
2319
|
+
: cell),
|
|
2320
|
+
generationPlans: (document.generationPlans ?? []).map((record) => record.status === "queued"
|
|
2321
|
+
&& planCellIds.has(record.cellId)
|
|
2322
|
+
&& record.planId === options.generationPlanId
|
|
2323
|
+
&& (generationPlanClaimRunId(record.plan) === options.runId
|
|
2324
|
+
|| (options.allowLegacyUnboundGenerationPlan === true
|
|
2325
|
+
&& generationPlanClaimRunId(record.plan) === null))
|
|
2326
|
+
? {
|
|
2327
|
+
...record,
|
|
2328
|
+
status: "failed",
|
|
2329
|
+
updatedAt,
|
|
2330
|
+
error: truncateGenerationError(options.error),
|
|
2331
|
+
}
|
|
2332
|
+
: record),
|
|
2333
|
+
notebookRunPlans: notebookRunPlanRecords(document).map((record) => record.planId === options.runPlanId
|
|
2334
|
+
&& record.status === "queued"
|
|
2335
|
+
&& record.runId === options.runId
|
|
2336
|
+
&& (selected.size === 0 || planCellIds.size > 0)
|
|
2337
|
+
? {
|
|
2338
|
+
...record,
|
|
2339
|
+
status: "failed",
|
|
2340
|
+
updatedAt,
|
|
2341
|
+
error: options.error,
|
|
2342
|
+
}
|
|
2343
|
+
: record),
|
|
2344
|
+
};
|
|
2345
|
+
}, {
|
|
2346
|
+
cellIds: [...selected],
|
|
2347
|
+
runId: options.runId,
|
|
2348
|
+
error: options.error.slice(0, 500),
|
|
2349
|
+
generationPlanId: options.generationPlanId,
|
|
2350
|
+
runPlanId: options.runPlanId,
|
|
2351
|
+
allowLegacyUnboundGenerationPlan: options.allowLegacyUnboundGenerationPlan,
|
|
2352
|
+
});
|
|
2353
|
+
if (!result.ok)
|
|
2354
|
+
throw new Error(result.error.message);
|
|
2355
|
+
return terminalized;
|
|
2356
|
+
}
|
|
2357
|
+
function notebookRunPlanRecords(document) {
|
|
2358
|
+
return document.notebookRunPlans ?? [];
|
|
2359
|
+
}
|
|
2360
|
+
async function updateNotebookRunPlanState(engine, notebookId, planId, status, details = {}) {
|
|
2361
|
+
const result = await mutateNotebook(engine, notebookId, "update_notebook_run_plan", (document) => ({
|
|
2362
|
+
...document,
|
|
2363
|
+
notebookRunPlans: notebookRunPlanRecords(document).map((record) => record.planId === planId
|
|
2364
|
+
&& typeof details.runId === "string"
|
|
2365
|
+
&& record.runId === details.runId
|
|
2366
|
+
? {
|
|
2367
|
+
...record,
|
|
2368
|
+
status,
|
|
2369
|
+
...details,
|
|
2370
|
+
updatedAt: new Date().toISOString(),
|
|
2371
|
+
}
|
|
2372
|
+
: record),
|
|
2373
|
+
}), { planId, status, ...details });
|
|
2374
|
+
if (!result.ok)
|
|
2375
|
+
throw new Error(result.error.message);
|
|
2376
|
+
}
|
|
2377
|
+
function paidCellAuthorized(document, cell, input) {
|
|
2378
|
+
if (!notebookCellIsPaid(cell))
|
|
2379
|
+
return true;
|
|
2380
|
+
const expectedFingerprint = notebookCellDefinitionFingerprint(document, cell);
|
|
2381
|
+
if (input.runPlanId) {
|
|
2382
|
+
const record = notebookRunPlanRecords(document).find((candidate) => candidate.planId === input.runPlanId);
|
|
2383
|
+
const fingerprints = record?.cellDefinitionFingerprints;
|
|
2384
|
+
return Boolean(record
|
|
2385
|
+
&& (record.status === "queued" || record.status === "awaiting-output")
|
|
2386
|
+
&& record.runId === input.runId
|
|
2387
|
+
&& fingerprints?.[cell.id] === expectedFingerprint);
|
|
2388
|
+
}
|
|
2389
|
+
const planId = input.generationPlan?.planId;
|
|
2390
|
+
if (typeof planId !== "string")
|
|
2391
|
+
return false;
|
|
2392
|
+
const records = document.generationPlans ?? [];
|
|
2393
|
+
const record = records.find((candidate) => candidate.planId === planId && candidate.cellId === cell.id);
|
|
2394
|
+
const plan = record?.plan;
|
|
2395
|
+
return Boolean(record
|
|
2396
|
+
&& (record.status === "queued" || record.status === "awaiting-output")
|
|
2397
|
+
&& generationPlanClaimRunId(record.plan) === input.runId
|
|
2398
|
+
&& plan
|
|
2399
|
+
&& typeof plan === "object"
|
|
2400
|
+
&& !Array.isArray(plan)
|
|
2401
|
+
&& (plan.cellDefinitionFingerprint
|
|
2402
|
+
?? plan.cellFingerprint) === expectedFingerprint);
|
|
2403
|
+
}
|
|
2404
|
+
function dispatchedPromptFor(cell, dispatchedPayload) {
|
|
2405
|
+
return submittedGenerationPrompt({
|
|
2406
|
+
dispatchedPrompt: payloadPrompt(dispatchedPayload),
|
|
2407
|
+
cellPrompt: cell.prompt,
|
|
2408
|
+
resolvedPrompt: cell.inputs?.resolvedPrompt,
|
|
2409
|
+
});
|
|
2410
|
+
}
|
|
2411
|
+
async function finishAwaitingNotebookCell(engine, input) {
|
|
2412
|
+
const read = engine.notebooks.read(input.notebookId);
|
|
2413
|
+
if (!read.ok)
|
|
2414
|
+
throw new Error(read.error.message);
|
|
2415
|
+
const resolvedTarget = resolveAwaitNotebookTarget(read.value, input);
|
|
2416
|
+
const cell = resolvedTarget.cell;
|
|
2417
|
+
if (!cell)
|
|
2418
|
+
throw new Error(`Awaiting notebook cell not found: ${input.cellId}`);
|
|
2419
|
+
input = {
|
|
2420
|
+
...input,
|
|
2421
|
+
cellId: cell.id,
|
|
2422
|
+
...(resolvedTarget.outputCellId
|
|
2423
|
+
? { outputCellId: resolvedTarget.outputCellId }
|
|
2424
|
+
: {}),
|
|
2425
|
+
};
|
|
2426
|
+
const initialOutput = isConcurrentGenerationProducer(cell)
|
|
2427
|
+
? findGenerationResultForRun(read.value, cell, {
|
|
2428
|
+
runId: input.runId,
|
|
2429
|
+
outputCellId: input.outputCellId,
|
|
2430
|
+
})
|
|
2431
|
+
: null;
|
|
2432
|
+
if (isConcurrentGenerationProducer(cell) && !initialOutput) {
|
|
2433
|
+
throw new Error(`Awaiting generation result not found for run ${input.runId}`);
|
|
2434
|
+
}
|
|
2435
|
+
const generationId = activeGenerationIdForRun(engine, input.notebookId, cell.id, input.runId, input.providerArtifactId);
|
|
2436
|
+
const current = executionState(read.value)[initialOutput?.outputCell.id ?? cell.id];
|
|
2437
|
+
if (current?.status === "completed"
|
|
2438
|
+
&& current.outputArtifactId
|
|
2439
|
+
&& engine.artifacts.get(current.outputArtifactId).ok) {
|
|
2440
|
+
await patchGeneration(engine, generationId, {
|
|
2441
|
+
status: "completed",
|
|
2442
|
+
outputArtifactId: current.outputArtifactId,
|
|
2443
|
+
});
|
|
2444
|
+
return {
|
|
2445
|
+
outputArtifactId: current.outputArtifactId,
|
|
2446
|
+
document: read.value,
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
let outputArtifactId = input.providerArtifactId;
|
|
2450
|
+
const payloadResult = await engine.metadata.artifacts.read(input.providerArtifactId, "generation-payload");
|
|
2451
|
+
const dispatchedPayload = payloadResult.ok ? payloadResult.value : null;
|
|
2452
|
+
const resolvedPrompt = dispatchedPromptFor(cell, dispatchedPayload);
|
|
2453
|
+
const cellInputs = {
|
|
2454
|
+
providerArtifactId: input.providerArtifactId,
|
|
2455
|
+
...(dispatchedPayload ? { dispatchedPayload } : {}),
|
|
2456
|
+
...(resolvedPrompt ? { resolvedPrompt } : {}),
|
|
2457
|
+
state: "completed",
|
|
2458
|
+
};
|
|
2459
|
+
if (notebookCellMediaType(cell) === "video") {
|
|
2460
|
+
const audioArtifactId = firstUpstreamArtifact(read.value, cell, "replace_audio");
|
|
2461
|
+
if (audioArtifactId) {
|
|
2462
|
+
const durationSeconds = Math.max(1, Number(cell.inputs?.duration ?? 15));
|
|
2463
|
+
const normalized = await normalizeSceneVideo(engine, input.providerArtifactId, audioArtifactId, `${notebookGridTag(cell.slot)} normalized`, durationSeconds);
|
|
2464
|
+
outputArtifactId = normalized.artifactId;
|
|
2465
|
+
cellInputs.normalizedFromArtifactId = input.providerArtifactId;
|
|
2466
|
+
cellInputs.normalizedDurationSeconds = normalized.durationSeconds;
|
|
2467
|
+
}
|
|
2468
|
+
}
|
|
2469
|
+
const completedAt = new Date().toISOString();
|
|
2470
|
+
const state = {
|
|
2471
|
+
fingerprint: current?.fingerprint
|
|
2472
|
+
?? notebookCellFingerprint(read.value, cell),
|
|
2473
|
+
status: "completed",
|
|
2474
|
+
outputArtifactId,
|
|
2475
|
+
providerArtifactId: input.providerArtifactId,
|
|
2476
|
+
runId: input.runId,
|
|
2477
|
+
completedAt,
|
|
2478
|
+
updatedAt: completedAt,
|
|
2479
|
+
tool: current?.tool ?? cell.tool ?? cell.model,
|
|
2480
|
+
};
|
|
2481
|
+
const updated = await mutateNotebook(engine, input.notebookId, "complete_notebook_provider_output", (document) => {
|
|
2482
|
+
let working = document;
|
|
2483
|
+
let resolvedOutput = findGenerationResultForRun(working, cell, {
|
|
2484
|
+
runId: input.runId,
|
|
2485
|
+
outputCellId: input.outputCellId,
|
|
2486
|
+
});
|
|
2487
|
+
if (cell.type === "generate_audio"
|
|
2488
|
+
&& !resolvedOutput) {
|
|
2489
|
+
const ensured = ensureGenerationMediaOutput(engine, working, cell);
|
|
2490
|
+
working = ensured.document;
|
|
2491
|
+
resolvedOutput = {
|
|
2492
|
+
outputCell: ensured.outputCell,
|
|
2493
|
+
outputEdge: ensured.outputEdge,
|
|
2494
|
+
};
|
|
2495
|
+
}
|
|
2496
|
+
const concurrent = isConcurrentGenerationProducer(cell);
|
|
2497
|
+
const executionCellId = concurrent && resolvedOutput
|
|
2498
|
+
? resolvedOutput.outputCell.id
|
|
2499
|
+
: cell.id;
|
|
2500
|
+
let withOutput = withExecution(working, executionCellId, state, concurrent || resolvedOutput ? undefined : outputArtifactId);
|
|
2501
|
+
if (resolvedOutput) {
|
|
2502
|
+
withOutput = withMediaOutput(withOutput, new Set([resolvedOutput.outputCell.id]), outputArtifactId, {
|
|
2503
|
+
state: "completed",
|
|
2504
|
+
...(resolvedPrompt ? { resolvedPrompt } : {}),
|
|
2505
|
+
...(dispatchedPayload ? { dispatchedPayload } : {}),
|
|
2506
|
+
});
|
|
2507
|
+
}
|
|
2508
|
+
const outputCellId = resolvedOutput?.outputCell.id;
|
|
2509
|
+
const actionBefore = withOutput.cells.find((candidate) => candidate.id === cell.id);
|
|
2510
|
+
const fillActionPrompt = Boolean(resolvedPrompt)
|
|
2511
|
+
&& !actionBefore?.prompt;
|
|
2512
|
+
const withCells = {
|
|
2513
|
+
...withOutput,
|
|
2514
|
+
cells: withOutput.cells.map((candidate) => {
|
|
2515
|
+
if (candidate.id === cell.id) {
|
|
2516
|
+
const nextInputs = { ...(candidate.inputs ?? {}) };
|
|
2517
|
+
if (!concurrent)
|
|
2518
|
+
Object.assign(nextInputs, cellInputs);
|
|
2519
|
+
else {
|
|
2520
|
+
delete nextInputs.state;
|
|
2521
|
+
delete nextInputs.providerArtifactId;
|
|
2522
|
+
if (resolvedPrompt) {
|
|
2523
|
+
nextInputs.resolvedPrompt = resolvedPrompt;
|
|
2524
|
+
}
|
|
2525
|
+
}
|
|
2526
|
+
return {
|
|
2527
|
+
...candidate,
|
|
2528
|
+
...(fillActionPrompt ? { prompt: resolvedPrompt } : {}),
|
|
2529
|
+
inputs: nextInputs,
|
|
2530
|
+
};
|
|
2531
|
+
}
|
|
2532
|
+
if (candidate.id === outputCellId && resolvedPrompt) {
|
|
2533
|
+
return { ...candidate, prompt: resolvedPrompt };
|
|
2534
|
+
}
|
|
2535
|
+
return candidate;
|
|
2536
|
+
}),
|
|
2537
|
+
};
|
|
2538
|
+
// Filling the empty prompt column changes the cell's fingerprint, so
|
|
2539
|
+
// the stored execution fingerprint must come from the updated document
|
|
2540
|
+
// — otherwise the completed cell would immediately read as stale.
|
|
2541
|
+
const finalCell = withCells.cells.find((candidate) => candidate.id === cell.id);
|
|
2542
|
+
const fingerprint = fillActionPrompt && finalCell
|
|
2543
|
+
? notebookCellFingerprint(withCells, finalCell)
|
|
2544
|
+
: state.fingerprint;
|
|
2545
|
+
const nextExecution = { ...executionState(withCells) };
|
|
2546
|
+
if (concurrent && nextExecution[cell.id]?.runId === input.runId) {
|
|
2547
|
+
delete nextExecution[cell.id];
|
|
2548
|
+
}
|
|
2549
|
+
return {
|
|
2550
|
+
...withCells,
|
|
2551
|
+
execution: {
|
|
2552
|
+
...nextExecution,
|
|
2553
|
+
...(concurrent
|
|
2554
|
+
? {}
|
|
2555
|
+
: {
|
|
2556
|
+
[cell.id]: {
|
|
2557
|
+
...state,
|
|
2558
|
+
fingerprint,
|
|
2559
|
+
outputArtifactId,
|
|
2560
|
+
},
|
|
2561
|
+
}),
|
|
2562
|
+
...(outputCellId
|
|
2563
|
+
? {
|
|
2564
|
+
[outputCellId]: {
|
|
2565
|
+
fingerprint,
|
|
2566
|
+
status: "completed",
|
|
2567
|
+
outputArtifactId,
|
|
2568
|
+
completedAt,
|
|
2569
|
+
updatedAt: completedAt,
|
|
2570
|
+
...(input.runId ? { runId: input.runId } : {}),
|
|
2571
|
+
tool: state.tool,
|
|
2572
|
+
},
|
|
2573
|
+
}
|
|
2574
|
+
: {}),
|
|
2575
|
+
},
|
|
2576
|
+
};
|
|
2577
|
+
}, {
|
|
2578
|
+
cellId: cell.id,
|
|
2579
|
+
providerArtifactId: input.providerArtifactId,
|
|
2580
|
+
outputArtifactId,
|
|
2581
|
+
runId: input.runId,
|
|
2582
|
+
tool: state.tool,
|
|
2583
|
+
}, { artifactId: outputArtifactId });
|
|
2584
|
+
if (!updated.ok)
|
|
2585
|
+
throw new Error(updated.error.message);
|
|
2586
|
+
await patchGeneration(engine, generationId, {
|
|
2587
|
+
status: "completed",
|
|
2588
|
+
outputArtifactId,
|
|
2589
|
+
});
|
|
2590
|
+
const refreshed = engine.notebooks.read(input.notebookId);
|
|
2591
|
+
if (!refreshed.ok)
|
|
2592
|
+
throw new Error(refreshed.error.message);
|
|
2593
|
+
return { outputArtifactId, document: refreshed.value };
|
|
2594
|
+
}
|
|
2595
|
+
async function failAwaitingNotebookCell(engine, input, message) {
|
|
2596
|
+
const updatedAt = new Date().toISOString();
|
|
2597
|
+
const result = await mutateNotebook(engine, input.notebookId, "fail_notebook_provider_output", (document) => {
|
|
2598
|
+
const resolvedTarget = resolveAwaitNotebookTarget(document, input);
|
|
2599
|
+
const cell = resolvedTarget.cell;
|
|
2600
|
+
if (!cell)
|
|
2601
|
+
return document;
|
|
2602
|
+
const outputCellId = resolvedTarget.outputCellId;
|
|
2603
|
+
const concurrent = isConcurrentGenerationProducer(cell);
|
|
2604
|
+
const runMatchedOutput = concurrent
|
|
2605
|
+
? document.cells.find((candidate) => candidate.id !== cell.id
|
|
2606
|
+
&& (candidate.inputs?.generationRunId === input.runId
|
|
2607
|
+
|| executionState(document)[candidate.id]?.runId === input.runId))
|
|
2608
|
+
: undefined;
|
|
2609
|
+
const failTargetId = (concurrent
|
|
2610
|
+
? outputCellId ?? runMatchedOutput?.id
|
|
2611
|
+
: undefined) ?? cell.id;
|
|
2612
|
+
const current = executionState(document)[failTargetId]
|
|
2613
|
+
?? executionState(document)[cell.id];
|
|
2614
|
+
const withFailure = withExecution(document, failTargetId, {
|
|
2615
|
+
fingerprint: current?.fingerprint
|
|
2616
|
+
?? notebookCellFingerprint(document, cell),
|
|
2617
|
+
status: "error",
|
|
2618
|
+
outputArtifactId: input.providerArtifactId,
|
|
2619
|
+
providerArtifactId: input.providerArtifactId,
|
|
2620
|
+
runId: input.runId,
|
|
2621
|
+
completedAt: updatedAt,
|
|
2622
|
+
updatedAt,
|
|
2623
|
+
tool: current?.tool ?? cell.tool ?? cell.model,
|
|
2624
|
+
error: message,
|
|
2625
|
+
}, isConcurrentGenerationProducer(cell) ? undefined : input.providerArtifactId);
|
|
2626
|
+
if (isConcurrentGenerationProducer(cell)) {
|
|
2627
|
+
const execution = { ...executionState(withFailure) };
|
|
2628
|
+
if (execution[cell.id]?.runId === input.runId
|
|
2629
|
+
&& execution[cell.id]?.status === "awaiting-output") {
|
|
2630
|
+
delete execution[cell.id];
|
|
2631
|
+
}
|
|
2632
|
+
return {
|
|
2633
|
+
...withFailure,
|
|
2634
|
+
execution,
|
|
2635
|
+
cells: withFailure.cells.map((candidate) => candidate.id === failTargetId
|
|
2636
|
+
? {
|
|
2637
|
+
...candidate,
|
|
2638
|
+
inputs: {
|
|
2639
|
+
...(candidate.inputs ?? {}),
|
|
2640
|
+
state: "error",
|
|
2641
|
+
},
|
|
2642
|
+
}
|
|
2643
|
+
: candidate),
|
|
2644
|
+
};
|
|
2645
|
+
}
|
|
2646
|
+
return rollbackGenerationMediaOutput(withFailure, cell, input.providerArtifactId, outputCellId);
|
|
2647
|
+
}, {
|
|
2648
|
+
cellId: input.cellId,
|
|
2649
|
+
providerArtifactId: input.providerArtifactId,
|
|
2650
|
+
runId: input.runId,
|
|
2651
|
+
error: message.slice(0, 500),
|
|
2652
|
+
}, { artifactId: input.providerArtifactId });
|
|
2653
|
+
if (!result.ok)
|
|
2654
|
+
throw new Error(result.error.message);
|
|
2655
|
+
await patchGeneration(engine, activeGenerationIdForRun(engine, input.notebookId, input.cellId, input.runId, input.providerArtifactId), { status: "failed", error: truncateGenerationError(message) });
|
|
2656
|
+
const read = engine.notebooks.read(input.notebookId);
|
|
2657
|
+
if (!read.ok)
|
|
2658
|
+
throw new Error(read.error.message);
|
|
2659
|
+
return read.value;
|
|
2660
|
+
}
|
|
2661
|
+
export async function failNotebookProviderOutput(engine, input) {
|
|
2662
|
+
return await failAwaitingNotebookCell(engine, input, notebookProviderFailureMessage(engine, input));
|
|
2663
|
+
}
|
|
2664
|
+
function runHasAwaitingCells(document, runId) {
|
|
2665
|
+
return Object.values(executionState(document)).some((execution) => execution.runId === runId && execution.status === "awaiting-output");
|
|
2666
|
+
}
|
|
2667
|
+
async function enqueueNotebookResume(input, document) {
|
|
2668
|
+
const producer = document.cells.find(cell => cell.id === input.cellId);
|
|
2669
|
+
if (producer?.inputs?.chainRunId === input.runId)
|
|
2670
|
+
return;
|
|
2671
|
+
if (runHasAwaitingCells(document, input.runId))
|
|
2672
|
+
return;
|
|
2673
|
+
await queue.enqueue(`resume-notebook-${input.runId}`, {
|
|
2674
|
+
type: "run_notebook",
|
|
2675
|
+
bookId: input.bookId,
|
|
2676
|
+
notebookId: input.notebookId,
|
|
2677
|
+
runId: input.runId,
|
|
2678
|
+
resume: true,
|
|
2679
|
+
force: false,
|
|
2680
|
+
...(input.cellIds ? { cellIds: input.cellIds } : {}),
|
|
2681
|
+
...(input.outputCellId ? { outputCellId: input.outputCellId } : {}),
|
|
2682
|
+
...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
|
|
2683
|
+
...(input.generationPlan ? { generationPlan: input.generationPlan } : {}),
|
|
2684
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
async function awaitNotebookOutput(engine, input, signal) {
|
|
2688
|
+
const deadline = Date.now() + 45 * 60_000;
|
|
2689
|
+
while (Date.now() < deadline) {
|
|
2690
|
+
signal.throwIfAborted();
|
|
2691
|
+
const status = await engine.status.get(input.providerArtifactId);
|
|
2692
|
+
if (!status.ok)
|
|
2693
|
+
throw new Error(status.error.message);
|
|
2694
|
+
if (status.value === "ready") {
|
|
2695
|
+
const completed = await finishAwaitingNotebookCell(engine, input);
|
|
2696
|
+
if (input.generationPlan
|
|
2697
|
+
&& !input.runPlanId
|
|
2698
|
+
&& typeof input.generationPlan.planId === "string") {
|
|
2699
|
+
await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, "continued", { outputArtifactId: completed.outputArtifactId, runId: input.runId });
|
|
2700
|
+
}
|
|
2701
|
+
await enqueueNotebookResume(input, completed.document);
|
|
2702
|
+
return;
|
|
2703
|
+
}
|
|
2704
|
+
if (status.value === "error") {
|
|
2705
|
+
const pending = engine.jobs.pending.read(input.providerArtifactId);
|
|
2706
|
+
const failure = engine.jobs.failures.read(input.providerArtifactId);
|
|
2707
|
+
if (!pending.ok || !pending.value || (failure.ok && failure.value)) {
|
|
2708
|
+
const document = await failNotebookProviderOutput(engine, input);
|
|
2709
|
+
await enqueueNotebookResume(input, document);
|
|
2710
|
+
return;
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
await delay(2_000, undefined, { signal });
|
|
2714
|
+
}
|
|
2715
|
+
const document = await failAwaitingNotebookCell(engine, input, `Provider output ${input.providerArtifactId} timed out`);
|
|
2716
|
+
await enqueueNotebookResume(input, document);
|
|
2717
|
+
}
|
|
2718
|
+
export function registerRunNotebookJob(engine) {
|
|
2719
|
+
registerJobHandler("await_notebook_output", async (payload, context) => {
|
|
2720
|
+
const input = payload;
|
|
2721
|
+
try {
|
|
2722
|
+
await awaitNotebookOutput(engine, input, context.signal);
|
|
2723
|
+
return { status: "completed" };
|
|
2724
|
+
}
|
|
2725
|
+
catch (cause) {
|
|
2726
|
+
const reason = context.signal.aborted ? context.signal.reason : cause;
|
|
2727
|
+
const message = reason instanceof Error ? reason.message : String(reason);
|
|
2728
|
+
if (context.signal.aborted && message === "Queue stopped")
|
|
2729
|
+
throw reason;
|
|
2730
|
+
try {
|
|
2731
|
+
await failAwaitingNotebookCell(engine, input, message);
|
|
2732
|
+
}
|
|
2733
|
+
catch {
|
|
2734
|
+
// Best-effort: surface the original await failure if we cannot write
|
|
2735
|
+
// execution state (for example, notebook already deleted).
|
|
2736
|
+
}
|
|
2737
|
+
throw cause;
|
|
2738
|
+
}
|
|
2739
|
+
});
|
|
2740
|
+
const runNotebookHandler = async (payload) => {
|
|
2741
|
+
const input = payload;
|
|
2742
|
+
const runId = input.runId ?? uuidv7();
|
|
2743
|
+
const initial = engine.notebooks.read(input.notebookId);
|
|
2744
|
+
if (!initial.ok)
|
|
2745
|
+
throw new Error(initial.error.message);
|
|
2746
|
+
let document = normalizeLegacyAnalyzeDocument(initial.value);
|
|
2747
|
+
if (document !== initial.value) {
|
|
2748
|
+
const normalized = await mutateNotebook(engine, input.notebookId, "normalize_legacy_analyze_cells", normalizeLegacyAnalyzeDocument);
|
|
2749
|
+
if (!normalized.ok)
|
|
2750
|
+
throw new Error(normalized.error.message);
|
|
2751
|
+
const refreshed = engine.notebooks.read(input.notebookId);
|
|
2752
|
+
if (!refreshed.ok)
|
|
2753
|
+
throw new Error(refreshed.error.message);
|
|
2754
|
+
document = refreshed.value;
|
|
2755
|
+
}
|
|
2756
|
+
const selected = input.cellIds ? new Set(input.cellIds) : null;
|
|
2757
|
+
const required = selected
|
|
2758
|
+
? requiredNotebookCellIds(engine, document, [...selected], input.force === true)
|
|
2759
|
+
: new Set();
|
|
2760
|
+
const ordered = topologicalOrder(document);
|
|
2761
|
+
const requestedOutputCellId = input.outputCellId
|
|
2762
|
+
?? generationPlanClaimOutputCellId(input.generationPlan ?? {})
|
|
2763
|
+
?? undefined;
|
|
2764
|
+
const outputs = {};
|
|
2765
|
+
const errors = [];
|
|
2766
|
+
const blocked = new Set();
|
|
2767
|
+
const waiting = new Set();
|
|
2768
|
+
for (const cell of ordered) {
|
|
2769
|
+
if (!notebookCellIsRunnable(cell)) {
|
|
2770
|
+
const output = resolveNotebookCellArtifactId(document, cell)
|
|
2771
|
+
?? cell.outputArtifactId
|
|
2772
|
+
?? cell.outputEntityId;
|
|
2773
|
+
if (output)
|
|
2774
|
+
outputs[cell.id] = output;
|
|
2775
|
+
continue;
|
|
2776
|
+
}
|
|
2777
|
+
if (selected && !required.has(cell.id))
|
|
2778
|
+
continue;
|
|
2779
|
+
const upstreamBlocked = document.edges
|
|
2780
|
+
.filter((edge) => edge.target === cell.id)
|
|
2781
|
+
.some((edge) => blocked.has(edge.source));
|
|
2782
|
+
if (upstreamBlocked) {
|
|
2783
|
+
blocked.add(cell.id);
|
|
2784
|
+
errors.push(`${notebookGridTag(cell.slot)}: blocked by failed upstream dependency`);
|
|
2785
|
+
continue;
|
|
2786
|
+
}
|
|
2787
|
+
const upstreamWaiting = document.edges
|
|
2788
|
+
.filter((edge) => edge.target === cell.id)
|
|
2789
|
+
.some((edge) => waiting.has(edge.source));
|
|
2790
|
+
if (upstreamWaiting) {
|
|
2791
|
+
waiting.add(cell.id);
|
|
2792
|
+
continue;
|
|
2793
|
+
}
|
|
2794
|
+
const invocationOutput = isConcurrentGenerationProducer(cell)
|
|
2795
|
+
? findGenerationResultForRun(document, cell, {
|
|
2796
|
+
runId,
|
|
2797
|
+
outputCellId: requestedOutputCellId,
|
|
2798
|
+
})
|
|
2799
|
+
: null;
|
|
2800
|
+
const invocationExecution = invocationOutput
|
|
2801
|
+
? executionState(document)[invocationOutput.outputCell.id]
|
|
2802
|
+
: undefined;
|
|
2803
|
+
const currentExecution = invocationExecution?.runId === runId
|
|
2804
|
+
? invocationExecution
|
|
2805
|
+
: executionState(document)[cell.id];
|
|
2806
|
+
if (currentExecution?.status === "awaiting-output"
|
|
2807
|
+
|| currentExecution?.status === "running") {
|
|
2808
|
+
waiting.add(cell.id);
|
|
2809
|
+
continue;
|
|
2810
|
+
}
|
|
2811
|
+
if (input.resume
|
|
2812
|
+
&& currentExecution?.runId === runId
|
|
2813
|
+
&& currentExecution.status === "error") {
|
|
2814
|
+
const message = currentExecution.error ?? "provider output failed";
|
|
2815
|
+
errors.push(`${notebookGridTag(cell.slot)}: ${message}`);
|
|
2816
|
+
blocked.add(cell.id);
|
|
2817
|
+
continue;
|
|
2818
|
+
}
|
|
2819
|
+
if (input.resume
|
|
2820
|
+
&& invocationOutput
|
|
2821
|
+
&& currentExecution?.runId === runId
|
|
2822
|
+
&& currentExecution.status === "completed") {
|
|
2823
|
+
const artifactId = currentExecution.outputArtifactId
|
|
2824
|
+
?? invocationOutput.outputCell.outputArtifactId;
|
|
2825
|
+
if (artifactId && engine.artifacts.get(artifactId).ok) {
|
|
2826
|
+
outputs[cell.id] = artifactId;
|
|
2827
|
+
continue;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
const stale = notebookCellIsStale(engine, document, cell);
|
|
2831
|
+
const forceCell = input.force === true
|
|
2832
|
+
&& (!selected || selected.has(cell.id));
|
|
2833
|
+
if (!forceCell && !stale) {
|
|
2834
|
+
const artifactId = resolveNotebookCellArtifactId(document, cell)
|
|
2835
|
+
?? cell.outputArtifactId;
|
|
2836
|
+
if (artifactId)
|
|
2837
|
+
outputs[cell.id] = artifactId;
|
|
2838
|
+
continue;
|
|
2839
|
+
}
|
|
2840
|
+
const paidAuthorized = paidCellAuthorized(document, cell, input);
|
|
2841
|
+
if (notebookCellIsPaid(cell) && !paidAuthorized) {
|
|
2842
|
+
const message = `${notebookGridTag(cell.slot)}: the saved plan no longer matches this cell's contents; rerun the cell to replan`;
|
|
2843
|
+
errors.push(message);
|
|
2844
|
+
blocked.add(cell.id);
|
|
2845
|
+
continue;
|
|
2846
|
+
}
|
|
2847
|
+
try {
|
|
2848
|
+
const result = await runNotebookCell(engine, input.bookId, input.notebookId, document, cell, {
|
|
2849
|
+
confirmPaid: paidAuthorized,
|
|
2850
|
+
runId,
|
|
2851
|
+
...(requestedOutputCellId
|
|
2852
|
+
? { outputCellId: requestedOutputCellId }
|
|
2853
|
+
: {}),
|
|
2854
|
+
});
|
|
2855
|
+
document = result.document;
|
|
2856
|
+
outputs[cell.id] = result.outputArtifactId;
|
|
2857
|
+
if (input.generationPlan
|
|
2858
|
+
&& !input.runPlanId
|
|
2859
|
+
&& typeof input.generationPlan.planId === "string") {
|
|
2860
|
+
await writeGenerationPlanProvenance(engine, input, cell, result.outputArtifactId);
|
|
2861
|
+
await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, result.status === "awaiting-output"
|
|
2862
|
+
? "awaiting-output"
|
|
2863
|
+
: "continued", { outputArtifactId: result.outputArtifactId, runId });
|
|
2864
|
+
}
|
|
2865
|
+
if (result.status === "awaiting-output") {
|
|
2866
|
+
waiting.add(cell.id);
|
|
2867
|
+
await queue.enqueue(`await-notebook-${runId}-${cell.id}-${result.outputArtifactId}`, {
|
|
2868
|
+
type: "await_notebook_output",
|
|
2869
|
+
bookId: input.bookId,
|
|
2870
|
+
notebookId: input.notebookId,
|
|
2871
|
+
cellId: cell.id,
|
|
2872
|
+
providerArtifactId: result.outputArtifactId,
|
|
2873
|
+
runId,
|
|
2874
|
+
...(requestedOutputCellId
|
|
2875
|
+
? {
|
|
2876
|
+
outputCellId: requestedOutputCellId,
|
|
2877
|
+
}
|
|
2878
|
+
: {}),
|
|
2879
|
+
...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
|
|
2880
|
+
...(input.generationPlan
|
|
2881
|
+
? { generationPlan: input.generationPlan }
|
|
2882
|
+
: {}),
|
|
2883
|
+
...(input.actor ? { actor: input.actor } : {}),
|
|
2884
|
+
...(input.cellIds ? { cellIds: input.cellIds } : {}),
|
|
2885
|
+
});
|
|
2886
|
+
}
|
|
2887
|
+
}
|
|
2888
|
+
catch (cause) {
|
|
2889
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2890
|
+
errors.push(`${notebookGridTag(cell.slot)}: ${message}`);
|
|
2891
|
+
blocked.add(cell.id);
|
|
2892
|
+
if (input.generationPlan
|
|
2893
|
+
&& !input.runPlanId
|
|
2894
|
+
&& typeof input.generationPlan.planId === "string") {
|
|
2895
|
+
await updateGenerationPlanState(engine, input.notebookId, input.generationPlan.planId, "failed", { error: message, runId });
|
|
2896
|
+
}
|
|
2897
|
+
const refreshed = engine.notebooks.read(input.notebookId);
|
|
2898
|
+
if (refreshed.ok)
|
|
2899
|
+
document = refreshed.value;
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (waiting.size > 0) {
|
|
2903
|
+
if (input.runPlanId) {
|
|
2904
|
+
await updateNotebookRunPlanState(engine, input.notebookId, input.runPlanId, "awaiting-output", {
|
|
2905
|
+
runId,
|
|
2906
|
+
outputs,
|
|
2907
|
+
...(errors.length > 0 ? { error: errors.join("\n") } : {}),
|
|
2908
|
+
});
|
|
2909
|
+
}
|
|
2910
|
+
if (errors.length > 0) {
|
|
2911
|
+
throw new Error(`Notebook run is awaiting output with cell errors:\n${errors.join("\n")}`);
|
|
2912
|
+
}
|
|
2913
|
+
return {
|
|
2914
|
+
id: runId,
|
|
2915
|
+
notebookId: input.notebookId,
|
|
2916
|
+
status: "awaiting-output",
|
|
2917
|
+
cellOrder: ordered.map((cell) => cell.id),
|
|
2918
|
+
outputs,
|
|
2919
|
+
waitingCellIds: [...waiting],
|
|
2920
|
+
errors,
|
|
2921
|
+
};
|
|
2922
|
+
}
|
|
2923
|
+
const now = new Date().toISOString();
|
|
2924
|
+
const run = {
|
|
2925
|
+
id: runId,
|
|
2926
|
+
notebookId: input.notebookId,
|
|
2927
|
+
status: errors.length === 0 ? "completed" : "failed",
|
|
2928
|
+
startedAt: now,
|
|
2929
|
+
completedAt: now,
|
|
2930
|
+
cellOrder: ordered.map((cell) => cell.id),
|
|
2931
|
+
outputs,
|
|
2932
|
+
...(errors.length > 0 ? { error: errors.join("\n") } : {}),
|
|
2933
|
+
};
|
|
2934
|
+
const recorded = await engine.notebooks.recordRun(run);
|
|
2935
|
+
if (!recorded.ok)
|
|
2936
|
+
throw new Error(recorded.error.message);
|
|
2937
|
+
if (input.runPlanId) {
|
|
2938
|
+
await updateNotebookRunPlanState(engine, input.notebookId, input.runPlanId, errors.length === 0 ? "completed" : "failed", {
|
|
2939
|
+
runId,
|
|
2940
|
+
outputs,
|
|
2941
|
+
...(errors.length > 0 ? { error: errors.join("\n") } : {}),
|
|
2942
|
+
});
|
|
2943
|
+
}
|
|
2944
|
+
if (errors.length > 0)
|
|
2945
|
+
throw new Error(errors.join("\n"));
|
|
2946
|
+
return run;
|
|
2947
|
+
};
|
|
2948
|
+
registerJobHandler("run_notebook", async (payload) => {
|
|
2949
|
+
const input = payload;
|
|
2950
|
+
try {
|
|
2951
|
+
return await runNotebookHandler(payload);
|
|
2952
|
+
}
|
|
2953
|
+
catch (cause) {
|
|
2954
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
2955
|
+
const generationPlanId = typeof input.generationPlan?.planId === "string"
|
|
2956
|
+
? input.generationPlan.planId
|
|
2957
|
+
: undefined;
|
|
2958
|
+
if (typeof input.runId === "string") {
|
|
2959
|
+
await failQueuedNotebookCells(engine, input.notebookId, {
|
|
2960
|
+
cellIds: input.cellIds ?? [],
|
|
2961
|
+
runId: input.runId,
|
|
2962
|
+
error: message,
|
|
2963
|
+
...(generationPlanId ? { generationPlanId } : {}),
|
|
2964
|
+
...(input.runPlanId ? { runPlanId: input.runPlanId } : {}),
|
|
2965
|
+
}).catch(() => undefined);
|
|
2966
|
+
}
|
|
2967
|
+
throw cause;
|
|
2968
|
+
}
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
export async function reconcileAwaitingNotebookOutputs(engine, bookId) {
|
|
2972
|
+
const activeJobs = engine.jobs.queue.list({
|
|
2973
|
+
states: ["queued", "running", "completing"],
|
|
2974
|
+
type: "await_notebook_output",
|
|
2975
|
+
});
|
|
2976
|
+
const failedJobs = engine.jobs.queue.list({
|
|
2977
|
+
states: ["failed", "aborted"],
|
|
2978
|
+
type: "await_notebook_output",
|
|
2979
|
+
});
|
|
2980
|
+
let enqueued = 0;
|
|
2981
|
+
for (const notebook of engine.notebooks.list()) {
|
|
2982
|
+
const execution = executionState(notebook);
|
|
2983
|
+
for (const [cellId, state] of Object.entries(execution)) {
|
|
2984
|
+
if (state.status !== "awaiting-output"
|
|
2985
|
+
|| !state.runId) {
|
|
2986
|
+
continue;
|
|
2987
|
+
}
|
|
2988
|
+
const stateRunId = state.runId;
|
|
2989
|
+
const cell = notebook.cells.find((candidate) => candidate.id === cellId);
|
|
2990
|
+
const providerArtifactId = state.providerArtifactId
|
|
2991
|
+
?? state.outputArtifactId
|
|
2992
|
+
?? cell?.outputArtifactId;
|
|
2993
|
+
if (!providerArtifactId)
|
|
2994
|
+
continue;
|
|
2995
|
+
const producerCellId = cell
|
|
2996
|
+
? concurrentProducerIdForCell(cell)
|
|
2997
|
+
: null;
|
|
2998
|
+
const coveredByActive = activeJobs.some((job) => awaitJobCoversExecution({
|
|
2999
|
+
payload: job.payload,
|
|
3000
|
+
notebookId: notebook.id,
|
|
3001
|
+
cellId,
|
|
3002
|
+
producerCellId,
|
|
3003
|
+
runId: stateRunId,
|
|
3004
|
+
}));
|
|
3005
|
+
if (coveredByActive)
|
|
3006
|
+
continue;
|
|
3007
|
+
const matchingFailed = failedJobs
|
|
3008
|
+
.filter((job) => awaitJobCoversExecution({
|
|
3009
|
+
payload: job.payload,
|
|
3010
|
+
notebookId: notebook.id,
|
|
3011
|
+
cellId,
|
|
3012
|
+
producerCellId,
|
|
3013
|
+
runId: stateRunId,
|
|
3014
|
+
}))
|
|
3015
|
+
.sort((left, right) => right.id - left.id);
|
|
3016
|
+
const latestFailed = matchingFailed[0];
|
|
3017
|
+
const awaitProducerId = producerCellId ?? cellId;
|
|
3018
|
+
const awaitOutputCellId = cell && isImageVideoGenerationResult(cell)
|
|
3019
|
+
? cellId
|
|
3020
|
+
: undefined;
|
|
3021
|
+
const interrupted = latestFailed?.state === "aborted"
|
|
3022
|
+
&& latestFailed.error?.message === "Queue stopped";
|
|
3023
|
+
if (latestFailed && !interrupted) {
|
|
3024
|
+
const failure = engine.jobs.failures.read(providerArtifactId);
|
|
3025
|
+
const message = failure.ok && failure.value
|
|
3026
|
+
? notebookProviderFailureMessage(engine, {
|
|
3027
|
+
notebookId: notebook.id, cellId: awaitProducerId, providerArtifactId,
|
|
3028
|
+
})
|
|
3029
|
+
: latestFailed.error?.message ?? `Notebook await ${latestFailed.state}`;
|
|
3030
|
+
await failAwaitingNotebookCell(engine, {
|
|
3031
|
+
type: "await_notebook_output",
|
|
3032
|
+
bookId,
|
|
3033
|
+
notebookId: notebook.id,
|
|
3034
|
+
cellId: awaitProducerId,
|
|
3035
|
+
providerArtifactId,
|
|
3036
|
+
runId: stateRunId,
|
|
3037
|
+
...(awaitOutputCellId ? { outputCellId: awaitOutputCellId } : {}),
|
|
3038
|
+
}, message);
|
|
3039
|
+
continue;
|
|
3040
|
+
}
|
|
3041
|
+
const recovered = await recoverNotebookProviderTask(engine, providerArtifactId);
|
|
3042
|
+
if (!recovered) {
|
|
3043
|
+
const status = await engine.status.get(providerArtifactId);
|
|
3044
|
+
const failure = engine.jobs.failures.read(providerArtifactId);
|
|
3045
|
+
const active = engine.jobs.queue.list({
|
|
3046
|
+
artifactId: providerArtifactId, states: ["queued", "running", "completing"],
|
|
3047
|
+
});
|
|
3048
|
+
if (status.ok && status.value !== "ready" && failure.ok && !failure.value
|
|
3049
|
+
&& active.length === 0) {
|
|
3050
|
+
const written = await engine.jobs.failures.write(providerArtifactId, {
|
|
3051
|
+
message: "Provider task could not be recovered after restart. No saved task ID or output is available.",
|
|
3052
|
+
});
|
|
3053
|
+
if (!written.ok)
|
|
3054
|
+
throw new Error(written.error.message);
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
const runPlan = notebookRunPlanRecords(notebook).find((record) => record.runId === stateRunId
|
|
3058
|
+
&& (record.status === "queued"
|
|
3059
|
+
|| record.status === "awaiting-output"));
|
|
3060
|
+
const generationRecords = notebook.generationPlans ?? [];
|
|
3061
|
+
const generationRecord = generationRecords.find((candidate) => (candidate.cellId === awaitProducerId || candidate.cellId === cellId)
|
|
3062
|
+
&& (candidate.status === "queued"
|
|
3063
|
+
|| candidate.status === "awaiting-output")
|
|
3064
|
+
&& (generationPlanClaimRunId(candidate.plan) === stateRunId
|
|
3065
|
+
|| generationPlanClaimRunId(candidate.plan) === null));
|
|
3066
|
+
const generationPlan = runPlan?.plan ?? generationRecord?.plan;
|
|
3067
|
+
await queue.enqueue(`reconcile-await-notebook-${stateRunId}-${awaitProducerId}-${awaitOutputCellId ?? cellId}-${Date.now()}`, {
|
|
3068
|
+
type: "await_notebook_output",
|
|
3069
|
+
bookId,
|
|
3070
|
+
notebookId: notebook.id,
|
|
3071
|
+
cellId: awaitProducerId,
|
|
3072
|
+
providerArtifactId,
|
|
3073
|
+
runId: stateRunId,
|
|
3074
|
+
...(awaitOutputCellId ? { outputCellId: awaitOutputCellId } : {}),
|
|
3075
|
+
...(typeof runPlan?.planId === "string"
|
|
3076
|
+
? { runPlanId: runPlan.planId }
|
|
3077
|
+
: {}),
|
|
3078
|
+
...(generationPlan ? { generationPlan } : {}),
|
|
3079
|
+
cellIds: [awaitProducerId],
|
|
3080
|
+
actor: "notebook-reconciler",
|
|
3081
|
+
});
|
|
3082
|
+
enqueued += 1;
|
|
3083
|
+
}
|
|
3084
|
+
}
|
|
3085
|
+
return enqueued;
|
|
3086
|
+
}
|
|
3087
|
+
export async function reconcileGenerationMediaOutputs(engine) {
|
|
3088
|
+
let repaired = 0;
|
|
3089
|
+
for (const notebook of engine.notebooks.list()) {
|
|
3090
|
+
const normalized = ensureGenerationMediaOutputs(engine, notebook);
|
|
3091
|
+
if (normalized === notebook)
|
|
3092
|
+
continue;
|
|
3093
|
+
const result = await mutateNotebook(engine, notebook.id, "reconcile_generation_media_outputs", (current) => ensureGenerationMediaOutputs(engine, current));
|
|
3094
|
+
if (!result.ok)
|
|
3095
|
+
throw new Error(result.error.message);
|
|
3096
|
+
repaired += 1;
|
|
3097
|
+
}
|
|
3098
|
+
return repaired;
|
|
3099
|
+
}
|
|
3100
|
+
async function notebookRunHasLiveWork(engine, document, runId, runPlanId) {
|
|
3101
|
+
if (notebookRunPlanRecords(document).some((record) => (record.runId === runId || record.planId === runPlanId)
|
|
3102
|
+
&& record.status === "awaiting-output"))
|
|
3103
|
+
return true;
|
|
3104
|
+
const activeJobs = engine.jobs.queue.list({
|
|
3105
|
+
states: ["queued", "running", "completing"],
|
|
3106
|
+
});
|
|
3107
|
+
if (activeJobs.some((job) => job.payload.notebookId === document.id
|
|
3108
|
+
&& job.payload.runId === runId
|
|
3109
|
+
&& (job.type === "run_notebook" || job.type === "await_notebook_output"))) {
|
|
3110
|
+
return true;
|
|
3111
|
+
}
|
|
3112
|
+
const pending = await engine.jobs.pending.findAll();
|
|
3113
|
+
if (!pending.ok)
|
|
3114
|
+
return true;
|
|
3115
|
+
const activeArtifactIds = new Set(pending.value.map((task) => task.artifactId));
|
|
3116
|
+
for (const job of activeJobs) {
|
|
3117
|
+
if (job.artifactId)
|
|
3118
|
+
activeArtifactIds.add(job.artifactId);
|
|
3119
|
+
}
|
|
3120
|
+
if (activeArtifactIds.size === 0)
|
|
3121
|
+
return false;
|
|
3122
|
+
const runArtifactIds = new Set();
|
|
3123
|
+
for (const state of Object.values(executionState(document))) {
|
|
3124
|
+
if (state.runId !== runId)
|
|
3125
|
+
continue;
|
|
3126
|
+
if (state.providerArtifactId)
|
|
3127
|
+
runArtifactIds.add(state.providerArtifactId);
|
|
3128
|
+
if (state.outputArtifactId)
|
|
3129
|
+
runArtifactIds.add(state.outputArtifactId);
|
|
3130
|
+
}
|
|
3131
|
+
for (const cell of document.cells) {
|
|
3132
|
+
const generations = engine.generations.listForCell(document.id, cell.id);
|
|
3133
|
+
if (!generations.ok)
|
|
3134
|
+
return true;
|
|
3135
|
+
for (const generation of generations.value) {
|
|
3136
|
+
if (generation.runId !== runId)
|
|
3137
|
+
continue;
|
|
3138
|
+
if (generation.providerArtifactId) {
|
|
3139
|
+
runArtifactIds.add(generation.providerArtifactId);
|
|
3140
|
+
}
|
|
3141
|
+
if (generation.outputArtifactId) {
|
|
3142
|
+
runArtifactIds.add(generation.outputArtifactId);
|
|
3143
|
+
}
|
|
3144
|
+
}
|
|
3145
|
+
}
|
|
3146
|
+
return [...runArtifactIds].some((artifactId) => activeArtifactIds.has(artifactId));
|
|
3147
|
+
}
|
|
3148
|
+
export async function reconcileQueuedNotebookCells(engine) {
|
|
3149
|
+
let terminalized = 0;
|
|
3150
|
+
for (const listed of engine.notebooks.list()) {
|
|
3151
|
+
for (const [cellId, listedState] of Object.entries(executionState(listed))) {
|
|
3152
|
+
if (listedState.status !== "queued" || !listedState.runId)
|
|
3153
|
+
continue;
|
|
3154
|
+
const read = engine.notebooks.read(listed.id);
|
|
3155
|
+
if (!read.ok)
|
|
3156
|
+
continue;
|
|
3157
|
+
const document = read.value;
|
|
3158
|
+
const state = executionState(document)[cellId];
|
|
3159
|
+
if (state?.status !== "queued" || state.runId !== listedState.runId)
|
|
3160
|
+
continue;
|
|
3161
|
+
const cell = document.cells.find((candidate) => candidate.id === cellId);
|
|
3162
|
+
const invocationResult = Boolean(cell && isImageVideoGenerationResult(cell));
|
|
3163
|
+
const producerCellId = cell
|
|
3164
|
+
? concurrentProducerIdForCell(cell) ?? cell.id
|
|
3165
|
+
: cellId;
|
|
3166
|
+
const producer = document.cells.find((candidate) => candidate.id === producerCellId) ?? cell;
|
|
3167
|
+
if (!cell || (!invocationResult && !notebookCellIsRunnable(cell)))
|
|
3168
|
+
continue;
|
|
3169
|
+
const terminalJobs = engine.jobs.queue.list({
|
|
3170
|
+
states: ["done", "failed", "aborted"],
|
|
3171
|
+
type: "run_notebook",
|
|
3172
|
+
});
|
|
3173
|
+
const matchingTerminal = terminalJobs
|
|
3174
|
+
.filter((job) => {
|
|
3175
|
+
if (job.payload.notebookId !== document.id
|
|
3176
|
+
|| job.payload.runId !== state.runId)
|
|
3177
|
+
return false;
|
|
3178
|
+
const cellIds = job.payload.cellIds;
|
|
3179
|
+
const jobOutputCellId = typeof job.payload.outputCellId === "string"
|
|
3180
|
+
? job.payload.outputCellId
|
|
3181
|
+
: null;
|
|
3182
|
+
return !Array.isArray(cellIds)
|
|
3183
|
+
|| cellIds.includes(cellId)
|
|
3184
|
+
|| cellIds.includes(producerCellId)
|
|
3185
|
+
|| jobOutputCellId === cellId;
|
|
3186
|
+
})
|
|
3187
|
+
.sort((left, right) => right.id - left.id);
|
|
3188
|
+
const terminalJob = matchingTerminal[0];
|
|
3189
|
+
if (terminalJob?.state === "done")
|
|
3190
|
+
continue;
|
|
3191
|
+
const claimedPlans = (document.generationPlans ?? []).filter((record) => record.cellId === producerCellId
|
|
3192
|
+
&& record.status === "queued"
|
|
3193
|
+
&& generationPlanClaimRunId(record.plan) === state.runId
|
|
3194
|
+
&& (!invocationResult
|
|
3195
|
+
|| generationPlanClaimOutputCellId(record.plan) === cellId
|
|
3196
|
+
|| generationPlanClaimOutputCellId(record.plan) === null));
|
|
3197
|
+
if (!terminalJob
|
|
3198
|
+
&& producer
|
|
3199
|
+
&& notebookCellIsPaid(producer)
|
|
3200
|
+
&& claimedPlans.length !== 1)
|
|
3201
|
+
continue;
|
|
3202
|
+
const runPlanId = typeof terminalJob?.payload.runPlanId === "string"
|
|
3203
|
+
? terminalJob.payload.runPlanId
|
|
3204
|
+
: undefined;
|
|
3205
|
+
if (await notebookRunHasLiveWork(engine, document, state.runId, runPlanId))
|
|
3206
|
+
continue;
|
|
3207
|
+
const latest = engine.notebooks.read(document.id);
|
|
3208
|
+
if (!latest.ok)
|
|
3209
|
+
continue;
|
|
3210
|
+
const latestState = executionState(latest.value)[cellId];
|
|
3211
|
+
if (latestState?.status !== "queued" || latestState.runId !== state.runId) {
|
|
3212
|
+
continue;
|
|
3213
|
+
}
|
|
3214
|
+
if (await notebookRunHasLiveWork(engine, latest.value, state.runId, runPlanId))
|
|
3215
|
+
continue;
|
|
3216
|
+
const message = terminalJob?.error?.message
|
|
3217
|
+
?? (terminalJob
|
|
3218
|
+
? `Notebook run ${terminalJob.state} before queued cell ${notebookGridTag(cell.slot)} started`
|
|
3219
|
+
: `Queued notebook run has no persisted job for ${notebookGridTag(cell.slot)}`);
|
|
3220
|
+
const plan = terminalJob?.payload.generationPlan;
|
|
3221
|
+
const terminalPlanId = plan
|
|
3222
|
+
&& typeof plan === "object"
|
|
3223
|
+
&& !Array.isArray(plan)
|
|
3224
|
+
&& typeof plan.planId === "string"
|
|
3225
|
+
? plan.planId
|
|
3226
|
+
: undefined;
|
|
3227
|
+
const generationPlanId = terminalPlanId ?? claimedPlans[0]?.planId;
|
|
3228
|
+
terminalized += await failQueuedNotebookCells(engine, document.id, {
|
|
3229
|
+
cellIds: [...new Set([cellId, producerCellId])],
|
|
3230
|
+
runId: state.runId,
|
|
3231
|
+
error: message,
|
|
3232
|
+
...(generationPlanId ? { generationPlanId } : {}),
|
|
3233
|
+
...(runPlanId ? { runPlanId } : {}),
|
|
3234
|
+
...(terminalJob ? { allowLegacyUnboundGenerationPlan: true } : {}),
|
|
3235
|
+
});
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
for (const listed of engine.notebooks.list()) {
|
|
3239
|
+
for (const listedPlan of notebookRunPlanRecords(listed)) {
|
|
3240
|
+
if (listedPlan.status !== "queued" || !listedPlan.runId)
|
|
3241
|
+
continue;
|
|
3242
|
+
const read = engine.notebooks.read(listed.id);
|
|
3243
|
+
if (!read.ok)
|
|
3244
|
+
continue;
|
|
3245
|
+
const document = read.value;
|
|
3246
|
+
const plan = notebookRunPlanRecords(document).find((candidate) => candidate.planId === listedPlan.planId);
|
|
3247
|
+
if (plan?.status !== "queued"
|
|
3248
|
+
|| plan.runId !== listedPlan.runId)
|
|
3249
|
+
continue;
|
|
3250
|
+
const terminalJobs = engine.jobs.queue.list({
|
|
3251
|
+
states: ["done", "failed", "aborted"],
|
|
3252
|
+
type: "run_notebook",
|
|
3253
|
+
});
|
|
3254
|
+
const terminalJob = terminalJobs
|
|
3255
|
+
.filter((job) => job.payload.notebookId === document.id
|
|
3256
|
+
&& job.payload.runId === plan.runId)
|
|
3257
|
+
.sort((left, right) => right.id - left.id)[0];
|
|
3258
|
+
if (terminalJob?.state === "done")
|
|
3259
|
+
continue;
|
|
3260
|
+
if (await notebookRunHasLiveWork(engine, document, plan.runId, plan.planId))
|
|
3261
|
+
continue;
|
|
3262
|
+
const latest = engine.notebooks.read(document.id);
|
|
3263
|
+
if (!latest.ok)
|
|
3264
|
+
continue;
|
|
3265
|
+
const latestPlan = notebookRunPlanRecords(latest.value).find((candidate) => candidate.planId === plan.planId);
|
|
3266
|
+
if (latestPlan?.status !== "queued"
|
|
3267
|
+
|| latestPlan.runId !== plan.runId)
|
|
3268
|
+
continue;
|
|
3269
|
+
if (await notebookRunHasLiveWork(engine, latest.value, plan.runId, plan.planId))
|
|
3270
|
+
continue;
|
|
3271
|
+
const message = terminalJob?.error?.message
|
|
3272
|
+
?? (terminalJob
|
|
3273
|
+
? `Notebook run ${terminalJob.state} before aggregate plan completed`
|
|
3274
|
+
: "Claimed aggregate notebook run has no persisted job");
|
|
3275
|
+
await failQueuedNotebookCells(engine, document.id, {
|
|
3276
|
+
cellIds: [],
|
|
3277
|
+
runId: plan.runId,
|
|
3278
|
+
runPlanId: plan.planId,
|
|
3279
|
+
error: message,
|
|
3280
|
+
});
|
|
3281
|
+
const reconciled = engine.notebooks.read(document.id);
|
|
3282
|
+
if (reconciled.ok
|
|
3283
|
+
&& notebookRunPlanRecords(reconciled.value).some((candidate) => candidate.planId === plan.planId && candidate.status === "failed"))
|
|
3284
|
+
terminalized += 1;
|
|
3285
|
+
}
|
|
3286
|
+
}
|
|
3287
|
+
return terminalized;
|
|
3288
|
+
}
|
|
3289
|
+
export async function abortLeasedNotebookRunsOnStartup(engine) {
|
|
3290
|
+
const leased = engine.jobs.queue.list({
|
|
3291
|
+
states: ["running", "completing"],
|
|
3292
|
+
type: "run_notebook",
|
|
3293
|
+
});
|
|
3294
|
+
let aborted = 0;
|
|
3295
|
+
for (const job of leased) {
|
|
3296
|
+
if (await engine.jobs.queue.abort(job.id, "Server restarted before notebook run completed"))
|
|
3297
|
+
aborted += 1;
|
|
3298
|
+
}
|
|
3299
|
+
return aborted;
|
|
3300
|
+
}
|
|
3301
|
+
export function topologicalOrder(notebook) {
|
|
3302
|
+
const cells = new Map(notebook.cells.map((cell) => [cell.id, cell]));
|
|
3303
|
+
const incoming = new Map(notebook.cells.map((cell) => [cell.id, 0]));
|
|
3304
|
+
const outgoing = new Map();
|
|
3305
|
+
for (const edge of notebook.edges) {
|
|
3306
|
+
if (!cells.has(edge.source) || !cells.has(edge.target)) {
|
|
3307
|
+
throw new Error(`Notebook edge references a missing cell: ${edge.id}`);
|
|
3308
|
+
}
|
|
3309
|
+
incoming.set(edge.target, (incoming.get(edge.target) ?? 0) + 1);
|
|
3310
|
+
const targets = outgoing.get(edge.source) ?? [];
|
|
3311
|
+
targets.push(edge.target);
|
|
3312
|
+
outgoing.set(edge.source, targets);
|
|
3313
|
+
}
|
|
3314
|
+
const ready = notebook.cells
|
|
3315
|
+
.filter((cell) => incoming.get(cell.id) === 0)
|
|
3316
|
+
.map((cell) => cell.id);
|
|
3317
|
+
const ordered = [];
|
|
3318
|
+
while (ready.length > 0) {
|
|
3319
|
+
const id = ready.shift();
|
|
3320
|
+
if (!id)
|
|
3321
|
+
continue;
|
|
3322
|
+
const cell = cells.get(id);
|
|
3323
|
+
if (cell)
|
|
3324
|
+
ordered.push(cell);
|
|
3325
|
+
for (const target of outgoing.get(id) ?? []) {
|
|
3326
|
+
const count = (incoming.get(target) ?? 0) - 1;
|
|
3327
|
+
incoming.set(target, count);
|
|
3328
|
+
if (count === 0)
|
|
3329
|
+
ready.push(target);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (ordered.length !== notebook.cells.length)
|
|
3333
|
+
throw new Error("Notebook graph contains a cycle");
|
|
3334
|
+
return ordered;
|
|
3335
|
+
}
|