@atomic-chat/core 0.13.2 → 0.17.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/README.md +1 -1
- package/dist/backend/advisor/advisor.d.ts +8 -7
- package/dist/backend/advisor/advisor.js +17 -25
- package/dist/backend/advisor/advisor.js.map +1 -1
- package/dist/backend/catalog/index.d.ts +4 -2
- package/dist/backend/catalog/index.js +4 -2
- package/dist/backend/catalog/index.js.map +1 -1
- package/dist/backend/catalog/manifest.d.ts +5 -51
- package/dist/backend/catalog/manifest.js +5 -93
- package/dist/backend/catalog/manifest.js.map +1 -1
- package/dist/backend/catalog/upstream-releases-baseline.d.ts +2 -0
- package/dist/backend/catalog/upstream-releases-baseline.js +12 -0
- package/dist/backend/catalog/upstream-releases-baseline.js.map +1 -0
- package/dist/backend/catalog/upstream-releases-baseline.json +101 -0
- package/dist/backend/catalog/upstream-releases-catalog.d.ts +54 -0
- package/dist/backend/catalog/upstream-releases-catalog.js +148 -0
- package/dist/backend/catalog/upstream-releases-catalog.js.map +1 -0
- package/dist/backend/catalog/upstream-releases.d.ts +75 -0
- package/dist/backend/catalog/upstream-releases.js +155 -0
- package/dist/backend/catalog/upstream-releases.js.map +1 -0
- package/dist/backend/install/service.d.ts +7 -4
- package/dist/backend/install/service.js +4 -4
- package/dist/backend/install/service.js.map +1 -1
- package/dist/cli/commands/serve.js +10 -28
- package/dist/cli/commands/serve.js.map +1 -1
- package/dist/cli/main.d.ts +1 -1
- package/dist/cli/main.js +0 -1
- package/dist/cli/main.js.map +1 -1
- package/dist/client/control-client.d.ts +7 -19
- package/dist/client/control-client.js +9 -29
- package/dist/client/control-client.js.map +1 -1
- package/dist/cloud/registry.d.ts +14 -1
- package/dist/cloud/registry.js +73 -2
- package/dist/cloud/registry.js.map +1 -1
- package/dist/config/paths.d.ts +4 -3
- package/dist/config/paths.js +15 -14
- package/dist/config/paths.js.map +1 -1
- package/dist/contracts/diffusion.d.ts +45 -18
- package/dist/contracts/diffusion.js +1 -1
- package/dist/contracts/diffusion.js.map +1 -1
- package/dist/contracts/engines.d.ts +1 -1
- package/dist/contracts/engines.js +2 -0
- package/dist/contracts/engines.js.map +1 -1
- package/dist/contracts/errors.d.ts +1 -3
- package/dist/contracts/errors.js +16 -0
- package/dist/contracts/errors.js.map +1 -1
- package/dist/contracts/events.d.ts +11 -6
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.js +1 -1
- package/dist/contracts/index.js.map +1 -1
- package/dist/contracts/media.d.ts +186 -0
- package/dist/contracts/media.js +55 -0
- package/dist/contracts/media.js.map +1 -0
- package/dist/contracts/session.d.ts +10 -1
- package/dist/contracts/session.js +10 -0
- package/dist/contracts/session.js.map +1 -1
- package/dist/contracts/settings.d.ts +36 -1
- package/dist/contracts/settings.js.map +1 -1
- package/dist/core/atomic-core.d.ts +7 -5
- package/dist/core/atomic-core.js +1 -0
- package/dist/core/atomic-core.js.map +1 -1
- package/dist/core/create.js +48 -58
- package/dist/core/create.js.map +1 -1
- package/dist/core/managed-engines.js +2 -0
- package/dist/core/managed-engines.js.map +1 -1
- package/dist/core/prism.d.ts +23 -6
- package/dist/core/prism.js +45 -0
- package/dist/core/prism.js.map +1 -1
- package/dist/core/public-server.js +3 -2
- package/dist/core/public-server.js.map +1 -1
- package/dist/core/types.d.ts +11 -0
- package/dist/core/types.js.map +1 -1
- package/dist/decision/process.d.ts +4 -0
- package/dist/decision/process.js +10 -1
- package/dist/decision/process.js.map +1 -1
- package/dist/diffusion/args.d.ts +3 -2
- package/dist/diffusion/args.js +21 -4
- package/dist/diffusion/args.js.map +1 -1
- package/dist/diffusion/compat.d.ts +1 -1
- package/dist/diffusion/compat.js +6 -2
- package/dist/diffusion/compat.js.map +1 -1
- package/dist/diffusion/gallery.d.ts +24 -3
- package/dist/diffusion/gallery.js +151 -21
- package/dist/diffusion/gallery.js.map +1 -1
- package/dist/diffusion/index.d.ts +3 -0
- package/dist/diffusion/index.js +3 -0
- package/dist/diffusion/index.js.map +1 -1
- package/dist/diffusion/jobs.d.ts +6 -1
- package/dist/diffusion/jobs.js +10 -1
- package/dist/diffusion/jobs.js.map +1 -1
- package/dist/diffusion/media-header.d.ts +22 -0
- package/dist/diffusion/media-header.js +149 -0
- package/dist/diffusion/media-header.js.map +1 -0
- package/dist/diffusion/parse.d.ts +3 -1
- package/dist/diffusion/parse.js +36 -0
- package/dist/diffusion/parse.js.map +1 -1
- package/dist/diffusion/recipe.d.ts +10 -1
- package/dist/diffusion/recipe.js +85 -21
- package/dist/diffusion/recipe.js.map +1 -1
- package/dist/diffusion/remote-job.d.ts +76 -0
- package/dist/diffusion/remote-job.js +643 -0
- package/dist/diffusion/remote-job.js.map +1 -0
- package/dist/diffusion/remote-store.d.ts +38 -0
- package/dist/diffusion/remote-store.js +91 -0
- package/dist/diffusion/remote-store.js.map +1 -0
- package/dist/diffusion/service.d.ts +9 -1
- package/dist/diffusion/service.js +49 -9
- package/dist/diffusion/service.js.map +1 -1
- package/dist/diffusion/state.d.ts +13 -1
- package/dist/diffusion/state.js +8 -0
- package/dist/diffusion/state.js.map +1 -1
- package/dist/diffusion/tracker.d.ts +6 -0
- package/dist/diffusion/tracker.js +11 -0
- package/dist/diffusion/tracker.js.map +1 -1
- package/dist/diffusion/validate.js +2 -2
- package/dist/diffusion/validate.js.map +1 -1
- package/dist/diffusion/video-gallery.d.ts +11 -4
- package/dist/diffusion/video-gallery.js +62 -25
- package/dist/diffusion/video-gallery.js.map +1 -1
- package/dist/diffusion/video-history.js +4 -1
- package/dist/diffusion/video-history.js.map +1 -1
- package/dist/diffusion/video-recipe.js +58 -25
- package/dist/diffusion/video-recipe.js.map +1 -1
- package/dist/diffusion/wiring.d.ts +3 -0
- package/dist/diffusion/wiring.js +1 -0
- package/dist/diffusion/wiring.js.map +1 -1
- package/dist/diffusion/workflow.d.ts +8 -2
- package/dist/diffusion/workflow.js +13 -5
- package/dist/diffusion/workflow.js.map +1 -1
- package/dist/downloads/downloader.d.ts +6 -1
- package/dist/downloads/downloader.js +12 -1
- package/dist/downloads/downloader.js.map +1 -1
- package/dist/embedding/process.d.ts +4 -0
- package/dist/embedding/process.js +10 -1
- package/dist/embedding/process.js.map +1 -1
- package/dist/media/adapters/a1111.d.ts +32 -0
- package/dist/media/adapters/a1111.js +207 -0
- package/dist/media/adapters/a1111.js.map +1 -0
- package/dist/media/adapters/aimlapi-videos.d.ts +34 -0
- package/dist/media/adapters/aimlapi-videos.js +172 -0
- package/dist/media/adapters/aimlapi-videos.js.map +1 -0
- package/dist/media/adapters/dashscope-image.d.ts +26 -0
- package/dist/media/adapters/dashscope-image.js +160 -0
- package/dist/media/adapters/dashscope-image.js.map +1 -0
- package/dist/media/adapters/dashscope-video.d.ts +47 -0
- package/dist/media/adapters/dashscope-video.js +251 -0
- package/dist/media/adapters/dashscope-video.js.map +1 -0
- package/dist/media/adapters/gemini-image.d.ts +27 -0
- package/dist/media/adapters/gemini-image.js +181 -0
- package/dist/media/adapters/gemini-image.js.map +1 -0
- package/dist/media/adapters/gemini-veo.d.ts +39 -0
- package/dist/media/adapters/gemini-veo.js +196 -0
- package/dist/media/adapters/gemini-veo.js.map +1 -0
- package/dist/media/adapters/hf-images.d.ts +85 -0
- package/dist/media/adapters/hf-images.js +439 -0
- package/dist/media/adapters/hf-images.js.map +1 -0
- package/dist/media/adapters/hf-videos.d.ts +44 -0
- package/dist/media/adapters/hf-videos.js +265 -0
- package/dist/media/adapters/hf-videos.js.map +1 -0
- package/dist/media/adapters/minimax-image.d.ts +26 -0
- package/dist/media/adapters/minimax-image.js +111 -0
- package/dist/media/adapters/minimax-image.js.map +1 -0
- package/dist/media/adapters/minimax-video-v2.d.ts +34 -0
- package/dist/media/adapters/minimax-video-v2.js +191 -0
- package/dist/media/adapters/minimax-video-v2.js.map +1 -0
- package/dist/media/adapters/minimax-video.d.ts +28 -0
- package/dist/media/adapters/minimax-video.js +155 -0
- package/dist/media/adapters/minimax-video.js.map +1 -0
- package/dist/media/adapters/mistral-agent-image.d.ts +33 -0
- package/dist/media/adapters/mistral-agent-image.js +108 -0
- package/dist/media/adapters/mistral-agent-image.js.map +1 -0
- package/dist/media/adapters/nvidia-genai.d.ts +28 -0
- package/dist/media/adapters/nvidia-genai.js +182 -0
- package/dist/media/adapters/nvidia-genai.js.map +1 -0
- package/dist/media/adapters/openai-images.d.ts +23 -0
- package/dist/media/adapters/openai-images.js +164 -0
- package/dist/media/adapters/openai-images.js.map +1 -0
- package/dist/media/adapters/openai-videos.d.ts +34 -0
- package/dist/media/adapters/openai-videos.js +175 -0
- package/dist/media/adapters/openai-videos.js.map +1 -0
- package/dist/media/adapters/openrouter-images.d.ts +25 -0
- package/dist/media/adapters/openrouter-images.js +161 -0
- package/dist/media/adapters/openrouter-images.js.map +1 -0
- package/dist/media/adapters/openrouter-videos.d.ts +43 -0
- package/dist/media/adapters/openrouter-videos.js +208 -0
- package/dist/media/adapters/openrouter-videos.js.map +1 -0
- package/dist/media/adapters/registry.d.ts +7 -0
- package/dist/media/adapters/registry.js +45 -0
- package/dist/media/adapters/registry.js.map +1 -0
- package/dist/media/adapters/types.d.ts +133 -0
- package/dist/media/adapters/types.js +15 -0
- package/dist/media/adapters/types.js.map +1 -0
- package/dist/media/adapters/xai-images.d.ts +19 -0
- package/dist/media/adapters/xai-images.js +83 -0
- package/dist/media/adapters/xai-images.js.map +1 -0
- package/dist/media/adapters/xai-videos.d.ts +29 -0
- package/dist/media/adapters/xai-videos.js +148 -0
- package/dist/media/adapters/xai-videos.js.map +1 -0
- package/dist/media/catalog-baseline.d.ts +2 -0
- package/dist/media/catalog-baseline.js +10 -0
- package/dist/media/catalog-baseline.js.map +1 -0
- package/dist/media/catalog-baseline.json +1819 -0
- package/dist/media/catalog-service.d.ts +42 -0
- package/dist/media/catalog-service.js +111 -0
- package/dist/media/catalog-service.js.map +1 -0
- package/dist/media/catalog.d.ts +50 -0
- package/dist/media/catalog.js +315 -0
- package/dist/media/catalog.js.map +1 -0
- package/dist/media/http.d.ts +125 -0
- package/dist/media/http.js +308 -0
- package/dist/media/http.js.map +1 -0
- package/dist/media/index.d.ts +14 -0
- package/dist/media/index.js +15 -0
- package/dist/media/index.js.map +1 -0
- package/dist/media/mask.d.ts +12 -0
- package/dist/media/mask.js +57 -0
- package/dist/media/mask.js.map +1 -0
- package/dist/media/service.d.ts +42 -0
- package/dist/media/service.js +144 -0
- package/dist/media/service.js.map +1 -0
- package/dist/models/compatibility/inspect.d.ts +11 -2
- package/dist/models/compatibility/inspect.js +44 -13
- package/dist/models/compatibility/inspect.js.map +1 -1
- package/dist/models/compatibility/service.d.ts +3 -0
- package/dist/models/compatibility/service.js +1 -0
- package/dist/models/compatibility/service.js.map +1 -1
- package/dist/models/gguf/tensors.d.ts +1 -0
- package/dist/models/gguf/tensors.js +4 -1
- package/dist/models/gguf/tensors.js.map +1 -1
- package/dist/models/hf.d.ts +3 -16
- package/dist/models/hf.js +4 -42
- package/dist/models/hf.js.map +1 -1
- package/dist/router/resolve.d.ts +15 -4
- package/dist/router/resolve.js +25 -3
- package/dist/router/resolve.js.map +1 -1
- package/dist/runtime/environment/linux-provisioner.js +7 -67
- package/dist/runtime/environment/linux-provisioner.js.map +1 -1
- package/dist/runtime/environment/runtime-removal-plan.d.ts +19 -0
- package/dist/runtime/environment/runtime-removal-plan.js +74 -0
- package/dist/runtime/environment/runtime-removal-plan.js.map +1 -0
- package/dist/runtime/environment/windows-provisioner.js +20 -4
- package/dist/runtime/environment/windows-provisioner.js.map +1 -1
- package/dist/runtime/managed-engines/spec.d.ts +2 -2
- package/dist/runtime/managed-engines/spec.js +6 -0
- package/dist/runtime/managed-engines/spec.js.map +1 -1
- package/dist/runtime/managed-models/compatibility.d.ts +10 -0
- package/dist/runtime/managed-models/compatibility.js +18 -0
- package/dist/runtime/managed-models/compatibility.js.map +1 -1
- package/dist/runtime/managed-text/adapter.d.ts +17 -0
- package/dist/runtime/managed-text/adapter.js.map +1 -1
- package/dist/runtime/managed-text/lifecycle.d.ts +8 -0
- package/dist/runtime/managed-text/lifecycle.js +38 -1
- package/dist/runtime/managed-text/lifecycle.js.map +1 -1
- package/dist/runtime/managed-text/readiness.d.ts +21 -1
- package/dist/runtime/managed-text/readiness.js +64 -0
- package/dist/runtime/managed-text/readiness.js.map +1 -1
- package/dist/runtime/managed-text/request-rules.d.ts +9 -7
- package/dist/runtime/managed-text/request-rules.js +58 -2
- package/dist/runtime/managed-text/request-rules.js.map +1 -1
- package/dist/runtime/managed-text/setting-values.d.ts +9 -0
- package/dist/runtime/managed-text/setting-values.js +44 -0
- package/dist/runtime/managed-text/setting-values.js.map +1 -0
- package/dist/runtime/sglang/adapter.d.ts +50 -0
- package/dist/runtime/sglang/adapter.js +393 -0
- package/dist/runtime/sglang/adapter.js.map +1 -0
- package/dist/runtime/sglang/engine.d.ts +16 -0
- package/dist/runtime/sglang/engine.js +38 -0
- package/dist/runtime/sglang/engine.js.map +1 -0
- package/dist/runtime/sglang/index.d.ts +11 -0
- package/dist/runtime/sglang/index.js +12 -0
- package/dist/runtime/sglang/index.js.map +1 -0
- package/dist/runtime/sglang/memory.d.ts +64 -0
- package/dist/runtime/sglang/memory.js +90 -0
- package/dist/runtime/sglang/memory.js.map +1 -0
- package/dist/runtime/sglang/settings.d.ts +49 -0
- package/dist/runtime/sglang/settings.js +118 -0
- package/dist/runtime/sglang/settings.js.map +1 -0
- package/dist/runtime/shared/external-sessions.js +1 -0
- package/dist/runtime/shared/external-sessions.js.map +1 -1
- package/dist/runtime/shared/ports.d.ts +10 -0
- package/dist/runtime/shared/ports.js +30 -0
- package/dist/runtime/shared/ports.js.map +1 -1
- package/dist/runtime/tensorrt-llm/adapter.d.ts +1 -4
- package/dist/runtime/tensorrt-llm/adapter.js +28 -11
- package/dist/runtime/tensorrt-llm/adapter.js.map +1 -1
- package/dist/runtime/tensorrt-llm/check.js +1 -0
- package/dist/runtime/tensorrt-llm/check.js.map +1 -1
- package/dist/runtime/tensorrt-llm/compatibility.d.ts +30 -6
- package/dist/runtime/tensorrt-llm/compatibility.js +41 -9
- package/dist/runtime/tensorrt-llm/compatibility.js.map +1 -1
- package/dist/runtime/tensorrt-llm/cuda-graphs.d.ts +32 -0
- package/dist/runtime/tensorrt-llm/cuda-graphs.js +33 -0
- package/dist/runtime/tensorrt-llm/cuda-graphs.js.map +1 -0
- package/dist/runtime/tensorrt-llm/engine.js +11 -5
- package/dist/runtime/tensorrt-llm/engine.js.map +1 -1
- package/dist/runtime/tensorrt-llm/kv-cache.d.ts +16 -0
- package/dist/runtime/tensorrt-llm/kv-cache.js +21 -1
- package/dist/runtime/tensorrt-llm/kv-cache.js.map +1 -1
- package/dist/runtime/vllm/adapter.js +2 -47
- package/dist/runtime/vllm/adapter.js.map +1 -1
- package/dist/runtime/vllm/memory.js +3 -16
- package/dist/runtime/vllm/memory.js.map +1 -1
- package/dist/runtime/vllm/settings.d.ts +7 -0
- package/dist/runtime/vllm/settings.js +2 -30
- package/dist/runtime/vllm/settings.js.map +1 -1
- package/dist/server/control/router.js +5 -4
- package/dist/server/control/router.js.map +1 -1
- package/dist/server/control/routes/media.d.ts +9 -0
- package/dist/server/control/routes/media.js +24 -0
- package/dist/server/control/routes/media.js.map +1 -0
- package/dist/server/control/routes/model-compatibility.d.ts +9 -0
- package/dist/server/control/routes/model-compatibility.js +31 -0
- package/dist/server/control/routes/model-compatibility.js.map +1 -0
- package/dist/server/control/types.d.ts +18 -20
- package/dist/server/http.js +15 -2
- package/dist/server/http.js.map +1 -1
- package/dist/server/public/gates.d.ts +6 -1
- package/dist/server/public/gates.js +7 -3
- package/dist/server/public/gates.js.map +1 -1
- package/dist/server/public/images-params.js +18 -1
- package/dist/server/public/images-params.js.map +1 -1
- package/dist/server/public/images.d.ts +11 -2
- package/dist/server/public/images.js +73 -9
- package/dist/server/public/images.js.map +1 -1
- package/dist/server/public/index.d.ts +1 -0
- package/dist/server/public/index.js +16 -7
- package/dist/server/public/index.js.map +1 -1
- package/dist/server/public/listing.js +1 -0
- package/dist/server/public/listing.js.map +1 -1
- package/dist/server/public/remote-media.d.ts +51 -0
- package/dist/server/public/remote-media.js +225 -0
- package/dist/server/public/remote-media.js.map +1 -0
- package/dist/server/public/responses.js +4 -2
- package/dist/server/public/responses.js.map +1 -1
- package/dist/server/public/static-assets.generated.js +1 -1
- package/dist/server/public/static-assets.generated.js.map +1 -1
- package/dist/server/public/trace.js +2 -0
- package/dist/server/public/trace.js.map +1 -1
- package/dist/server/public/types.d.ts +12 -1
- package/dist/server/public/videos-params.js +13 -3
- package/dist/server/public/videos-params.js.map +1 -1
- package/dist/server/public/videos.d.ts +1 -0
- package/dist/server/public/videos.js +21 -3
- package/dist/server/public/videos.js.map +1 -1
- package/dist/settings/schema/llamacpp-upstream.json +9 -0
- package/dist/settings/schema/sglang.json +251 -0
- package/dist/settings/schema.js +27 -1
- package/dist/settings/schema.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -6
- package/src/settings/schema/llamacpp-upstream.json +9 -0
- package/src/settings/schema/sglang.json +251 -0
- package/dist/backend/catalog/bundled-manifest-baseline.d.ts +0 -2
- package/dist/backend/catalog/bundled-manifest-baseline.js +0 -65
- package/dist/backend/catalog/bundled-manifest-baseline.js.map +0 -1
- package/dist/contracts/model-setup.d.ts +0 -115
- package/dist/contracts/model-setup.js +0 -33
- package/dist/contracts/model-setup.js.map +0 -1
- package/dist/core/model-setup/index.d.ts +0 -4
- package/dist/core/model-setup/index.js +0 -5
- package/dist/core/model-setup/index.js.map +0 -1
- package/dist/core/model-setup/plan.d.ts +0 -56
- package/dist/core/model-setup/plan.js +0 -158
- package/dist/core/model-setup/plan.js.map +0 -1
- package/dist/core/model-setup/service.d.ts +0 -92
- package/dist/core/model-setup/service.js +0 -329
- package/dist/core/model-setup/service.js.map +0 -1
- package/dist/core/model-setup/store.d.ts +0 -29
- package/dist/core/model-setup/store.js +0 -116
- package/dist/core/model-setup/store.js.map +0 -1
- package/dist/core/model-setup/wiring.d.ts +0 -59
- package/dist/core/model-setup/wiring.js +0 -132
- package/dist/core/model-setup/wiring.js.map +0 -1
- package/dist/server/control/routes/model-setups.d.ts +0 -11
- package/dist/server/control/routes/model-setups.js +0 -55
- package/dist/server/control/routes/model-setups.js.map +0 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Remote jobs: an image or a clip a cloud or self-hosted provider makes for a request whose
|
|
3
|
+
* `target` names it (ADR 2026-10-13-remote-media-shares-the-diffusion-job-table). The records live in
|
|
4
|
+
* the same job table as the local runner's, go out as the same `diffusion:*` events and land in the
|
|
5
|
+
* same galleries, so the Images and Video pages, `/v1/images/generations` and `/v1/videos` see them
|
|
6
|
+
* the way they see a local job. What differs:
|
|
7
|
+
*
|
|
8
|
+
* - no `sd-server`, no `activeJobId`, no GPU claim, no idle timer: a remote job never blocks a local
|
|
9
|
+
* one, and up to `MAX_REMOTE_JOBS` run at once;
|
|
10
|
+
* - the request that starts a generation is never retried (the provider would bill twice);
|
|
11
|
+
* - cancel aborts every request and wait, then asks the provider to stop, best effort;
|
|
12
|
+
* - a video job is written to `remote-jobs.json` once submitted and polled again after a restart.
|
|
13
|
+
*
|
|
14
|
+
* The adapters that speak each provider live in `src/media/`; this file sees them through
|
|
15
|
+
* `RemoteMediaGateway`, injected by the owner.
|
|
16
|
+
*/
|
|
17
|
+
import { randomUUID } from 'node:crypto';
|
|
18
|
+
import { cancelledError, diffusionError, errorBody } from './errors.js';
|
|
19
|
+
import { IMAGE_JOB_KIND, resolveSource, withoutSources } from './image-job.js';
|
|
20
|
+
import { finishJob, setJobState } from './jobs.js';
|
|
21
|
+
import { imageMime, imageSize, mp4Facts } from './media-header.js';
|
|
22
|
+
import { isTerminalJobState } from './state.js';
|
|
23
|
+
import { VIDEO_JOB_KIND, withoutVideoSources } from './video-job.js';
|
|
24
|
+
/** Remote jobs running at once, images and clips together. */
|
|
25
|
+
export const MAX_REMOTE_JOBS = 4;
|
|
26
|
+
/** A clip still not done after this long is given up on. */
|
|
27
|
+
export const REMOTE_VIDEO_CEILING_MS = 3 * 60 * 60 * 1000;
|
|
28
|
+
/** Polls that may fail in a row (network, 5xx, rate limit) before the job fails. */
|
|
29
|
+
export const MAX_POLL_FAILURES = 10;
|
|
30
|
+
/** Progress goes out at least this often while a remote job runs. */
|
|
31
|
+
export const REMOTE_HEARTBEAT_MS = 1_000;
|
|
32
|
+
/** How long a resumed job waits for the app to configure the galleries before saving to the defaults. */
|
|
33
|
+
export const RESUME_CONFIG_WAIT_MS = 60_000;
|
|
34
|
+
function remoteDeps(deps) {
|
|
35
|
+
if (!deps.remote)
|
|
36
|
+
throw diffusionError('REMOTE_NOT_CONFIGURED', 'This core does not generate with cloud or self-hosted models.');
|
|
37
|
+
return deps.remote;
|
|
38
|
+
}
|
|
39
|
+
/** A fraction that creeps toward `cap` over time, for providers that say nothing about progress. */
|
|
40
|
+
export function timeFraction(elapsedMs, scaleMs, cap = 0.95) {
|
|
41
|
+
if (elapsedMs <= 0)
|
|
42
|
+
return 0;
|
|
43
|
+
return Math.min(cap, cap * (1 - Math.exp(-elapsedMs / scaleMs)));
|
|
44
|
+
}
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Validation and the adapter's job, pure
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
const IMAGE_INPUT_WORKFLOWS = ['edit', 'reference', 'transform', 'inpaint'];
|
|
49
|
+
/** Refuse what the model cannot do before anything is sent; the workflow it runs. */
|
|
50
|
+
export function checkRemoteImageRequest(request, model) {
|
|
51
|
+
if (request.prompt.trim() === '')
|
|
52
|
+
throw diffusionError('INVALID_REQUEST', 'Write a prompt first.');
|
|
53
|
+
const workflow = (request.workflow ?? 'create');
|
|
54
|
+
if (!model.workflows.includes(workflow))
|
|
55
|
+
throw diffusionError('UNSUPPORTED_WORKFLOW', `${model.name} cannot do that.`, workflow);
|
|
56
|
+
if (IMAGE_INPUT_WORKFLOWS.includes(workflow) && request.initImage === undefined)
|
|
57
|
+
throw diffusionError('INVALID_REQUEST', 'This workflow needs a source image.', workflow);
|
|
58
|
+
if (workflow === 'inpaint' && request.maskImage === undefined)
|
|
59
|
+
throw diffusionError('INVALID_REQUEST', 'Inpainting needs a mask.');
|
|
60
|
+
const inputs = (request.initImage ? 1 : 0) + (request.referenceImages?.length ?? 0);
|
|
61
|
+
const most = model.params.max_input_images;
|
|
62
|
+
if (inputs > 1 && most !== undefined && inputs > most)
|
|
63
|
+
throw diffusionError('INVALID_REQUEST', `${model.name} takes at most ${most} images.`, `${inputs} given`);
|
|
64
|
+
const most_n = model.params.n?.max ?? 1;
|
|
65
|
+
if (request.batchSize > most_n)
|
|
66
|
+
throw diffusionError('INVALID_REQUEST', `${model.name} makes at most ${most_n} image(s) at a time.`);
|
|
67
|
+
return workflow;
|
|
68
|
+
}
|
|
69
|
+
/** The adapter's job out of the request, the model's defaults filling what the request left out. */
|
|
70
|
+
export function imageGenerationJob(request, model, workflow, inputs, seed) {
|
|
71
|
+
const { params } = model;
|
|
72
|
+
const remote = request.remote ?? {};
|
|
73
|
+
const job = {
|
|
74
|
+
model,
|
|
75
|
+
prompt: request.prompt,
|
|
76
|
+
workflow,
|
|
77
|
+
n: Math.max(1, request.batchSize),
|
|
78
|
+
images: inputs.images,
|
|
79
|
+
};
|
|
80
|
+
if (inputs.mask)
|
|
81
|
+
job.mask = inputs.mask;
|
|
82
|
+
if (request.negativePrompt && params.negative_prompt)
|
|
83
|
+
job.negativePrompt = request.negativePrompt;
|
|
84
|
+
if (seed !== undefined)
|
|
85
|
+
job.seed = seed;
|
|
86
|
+
if (params.size?.kind === 'range' && request.width > 0 && request.height > 0) {
|
|
87
|
+
job.width = request.width;
|
|
88
|
+
job.height = request.height;
|
|
89
|
+
}
|
|
90
|
+
const size = remote.size ?? (params.size?.kind === 'enum' ? params.size.default : undefined);
|
|
91
|
+
if (size !== undefined && params.size?.kind === 'enum')
|
|
92
|
+
job.size = size;
|
|
93
|
+
const pick = (value, choice) => value ?? choice?.default;
|
|
94
|
+
const aspect = pick(remote.aspectRatio, params.aspect_ratios);
|
|
95
|
+
if (aspect && params.aspect_ratios)
|
|
96
|
+
job.aspectRatio = aspect;
|
|
97
|
+
const resolution = pick(remote.resolution, params.resolutions);
|
|
98
|
+
if (resolution && params.resolutions)
|
|
99
|
+
job.resolution = resolution;
|
|
100
|
+
if (remote.quality && params.quality)
|
|
101
|
+
job.quality = remote.quality;
|
|
102
|
+
if (remote.background && params.background)
|
|
103
|
+
job.background = remote.background;
|
|
104
|
+
// PNG keeps the recipe inside the image and gets a thumbnail; ask for it when the model offers it.
|
|
105
|
+
const format = remote.outputFormat ?? (params.output_format?.values.includes('png') ? 'png' : undefined);
|
|
106
|
+
if (format && params.output_format)
|
|
107
|
+
job.outputFormat = format;
|
|
108
|
+
if (params.steps && request.steps > 0)
|
|
109
|
+
job.steps = request.steps;
|
|
110
|
+
if (params.cfg && request.cfgScale > 0)
|
|
111
|
+
job.cfgScale = request.cfgScale;
|
|
112
|
+
if (params.guidance && request.guidance !== undefined)
|
|
113
|
+
job.guidance = request.guidance;
|
|
114
|
+
if (params.sampler && request.samplingMethod)
|
|
115
|
+
job.sampler = request.samplingMethod;
|
|
116
|
+
if (params.strength && (workflow === 'transform' || workflow === 'inpaint'))
|
|
117
|
+
job.strength = request.strength ?? params.strength.default ?? 0.75;
|
|
118
|
+
return job;
|
|
119
|
+
}
|
|
120
|
+
/** Refuse what the model cannot do before anything is sent; the workflow it runs. */
|
|
121
|
+
export function checkRemoteVideoRequest(request, model) {
|
|
122
|
+
if (request.prompt.trim() === '' && request.initImage === undefined)
|
|
123
|
+
throw diffusionError('INVALID_REQUEST', 'Write a prompt first.');
|
|
124
|
+
const workflow = (request.workflow ?? 'create');
|
|
125
|
+
if (!model.workflows.includes(workflow))
|
|
126
|
+
throw diffusionError('UNSUPPORTED_WORKFLOW', `${model.name} cannot do that.`, workflow);
|
|
127
|
+
if (workflow === 'image-to-video' && request.initImage === undefined)
|
|
128
|
+
throw diffusionError('INVALID_REQUEST', 'Image to video needs a first frame.');
|
|
129
|
+
const seconds = request.remote?.durationSecs;
|
|
130
|
+
const durations = model.params.durations;
|
|
131
|
+
if (seconds !== undefined && durations) {
|
|
132
|
+
const listed = durations.values;
|
|
133
|
+
const ok = listed
|
|
134
|
+
? listed.includes(seconds)
|
|
135
|
+
: seconds >= (durations.min ?? 0) && seconds <= (durations.max ?? Number.POSITIVE_INFINITY);
|
|
136
|
+
if (!ok)
|
|
137
|
+
throw diffusionError('INVALID_REQUEST', `${model.name} cannot make a clip of ${seconds} s.`);
|
|
138
|
+
}
|
|
139
|
+
return workflow;
|
|
140
|
+
}
|
|
141
|
+
/** The adapter's job out of the request, the model's defaults filling what the request left out. */
|
|
142
|
+
export function videoGenerationJob(request, model, workflow, frames, seed) {
|
|
143
|
+
const { params } = model;
|
|
144
|
+
const remote = request.remote ?? {};
|
|
145
|
+
const job = { model, prompt: request.prompt, workflow };
|
|
146
|
+
if (request.negativePrompt && params.negative_prompt)
|
|
147
|
+
job.negativePrompt = request.negativePrompt;
|
|
148
|
+
if (seed !== undefined)
|
|
149
|
+
job.seed = seed;
|
|
150
|
+
const seconds = remote.durationSecs ?? params.durations?.default;
|
|
151
|
+
if (seconds !== undefined && params.durations)
|
|
152
|
+
job.durationSecs = seconds;
|
|
153
|
+
const resolution = remote.resolution ?? params.resolutions?.default;
|
|
154
|
+
if (resolution && params.resolutions)
|
|
155
|
+
job.resolution = resolution;
|
|
156
|
+
const aspect = remote.aspectRatio ?? params.aspect_ratios?.default;
|
|
157
|
+
if (aspect && params.aspect_ratios)
|
|
158
|
+
job.aspectRatio = aspect;
|
|
159
|
+
const size = remote.size ?? params.sizes?.default;
|
|
160
|
+
if (size && params.sizes)
|
|
161
|
+
job.size = size;
|
|
162
|
+
if (params.audio && remote.generateAudio !== undefined)
|
|
163
|
+
job.generateAudio = remote.generateAudio;
|
|
164
|
+
if (workflow === 'image-to-video') {
|
|
165
|
+
if (frames.first)
|
|
166
|
+
job.firstFrame = frames.first;
|
|
167
|
+
if (frames.last && params.last_frame)
|
|
168
|
+
job.lastFrame = frames.last;
|
|
169
|
+
}
|
|
170
|
+
return job;
|
|
171
|
+
}
|
|
172
|
+
// ---------------------------------------------------------------------------
|
|
173
|
+
// Bookkeeping
|
|
174
|
+
// ---------------------------------------------------------------------------
|
|
175
|
+
function checkCapacity(deps, remote) {
|
|
176
|
+
const most = remote.maxJobs ?? MAX_REMOTE_JOBS;
|
|
177
|
+
if (deps.state.liveRemoteJobs() >= most)
|
|
178
|
+
throw diffusionError('JOB_BUSY', `${most} cloud generations are already running. Wait for one to finish.`);
|
|
179
|
+
}
|
|
180
|
+
async function readInput(source, deps) {
|
|
181
|
+
const bytes = Buffer.from(await resolveSource(source, deps), 'base64');
|
|
182
|
+
return { bytes, mime: imageMime(bytes) ?? 'image/png' };
|
|
183
|
+
}
|
|
184
|
+
/** A seed for the job when the model takes one: the request's, else a drawn one, so the recipe has it. */
|
|
185
|
+
function seedFor(deps, requested, takesSeed) {
|
|
186
|
+
if (!takesSeed)
|
|
187
|
+
return undefined;
|
|
188
|
+
return requested !== undefined && requested >= 0 ? requested : deps.drawSeed();
|
|
189
|
+
}
|
|
190
|
+
/** The job's outcome on its record and in the events; the runner's `done` value. */
|
|
191
|
+
function settle(deps, id, outcome) {
|
|
192
|
+
if (outcome.ok) {
|
|
193
|
+
finishJob(deps, id, { ok: true, outputs: outcome.outputs });
|
|
194
|
+
const job = deps.state.anyJob(id);
|
|
195
|
+
return { ok: true, outcome: { job, images: outcome.bytes } };
|
|
196
|
+
}
|
|
197
|
+
const error = errorBody(outcome.raw);
|
|
198
|
+
finishJob(deps, id, { ok: false, error });
|
|
199
|
+
if (error.code !== 'CANCELLED')
|
|
200
|
+
deps.emit('diffusion:error', { ...error, jobId: id });
|
|
201
|
+
return { ok: false, error: deps.state.anyJob(id)?.error ?? error };
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Progress for a remote job: what the provider says when it says it, else a fraction that creeps
|
|
205
|
+
* forward with time; never backwards, and out at least every `REMOTE_HEARTBEAT_MS`.
|
|
206
|
+
*/
|
|
207
|
+
class RemoteProgressFeed {
|
|
208
|
+
deps;
|
|
209
|
+
id;
|
|
210
|
+
kind;
|
|
211
|
+
startedAt;
|
|
212
|
+
scaleMs;
|
|
213
|
+
batchSize;
|
|
214
|
+
reported = 0;
|
|
215
|
+
provider;
|
|
216
|
+
phase = 'queued';
|
|
217
|
+
timer;
|
|
218
|
+
constructor(deps, id, kind, startedAt, scaleMs, batchSize = 1) {
|
|
219
|
+
this.deps = deps;
|
|
220
|
+
this.id = id;
|
|
221
|
+
this.kind = kind;
|
|
222
|
+
this.startedAt = startedAt;
|
|
223
|
+
this.scaleMs = scaleMs;
|
|
224
|
+
this.batchSize = batchSize;
|
|
225
|
+
}
|
|
226
|
+
start() {
|
|
227
|
+
this.emit();
|
|
228
|
+
this.timer = setInterval(() => this.emit(), REMOTE_HEARTBEAT_MS);
|
|
229
|
+
this.timer.unref?.();
|
|
230
|
+
}
|
|
231
|
+
update(fraction, phase) {
|
|
232
|
+
if (fraction !== undefined && Number.isFinite(fraction))
|
|
233
|
+
this.provider = Math.min(Math.max(fraction, 0), 0.99);
|
|
234
|
+
if (phase)
|
|
235
|
+
this.phase = phase === 'queued' ? 'queued' : 'sampling';
|
|
236
|
+
this.emit();
|
|
237
|
+
}
|
|
238
|
+
saving() {
|
|
239
|
+
this.phase = 'sampling';
|
|
240
|
+
this.reported = Math.max(this.reported, 0.99);
|
|
241
|
+
this.emit('saving');
|
|
242
|
+
}
|
|
243
|
+
stop() {
|
|
244
|
+
if (this.timer)
|
|
245
|
+
clearInterval(this.timer);
|
|
246
|
+
this.timer = undefined;
|
|
247
|
+
}
|
|
248
|
+
emit(phase) {
|
|
249
|
+
const record = this.deps.state.record(this.id);
|
|
250
|
+
if (!record || isTerminalJobState(record.job.state))
|
|
251
|
+
return this.stop();
|
|
252
|
+
const elapsedMs = Math.max(this.deps.now() - this.startedAt, 0);
|
|
253
|
+
const guess = timeFraction(elapsedMs, this.scaleMs);
|
|
254
|
+
this.reported = Math.max(this.reported, this.provider ?? guess);
|
|
255
|
+
const common = {
|
|
256
|
+
phase: phase ?? this.phase,
|
|
257
|
+
step: 0,
|
|
258
|
+
totalSteps: 0,
|
|
259
|
+
fraction: this.reported,
|
|
260
|
+
etaSeconds: null,
|
|
261
|
+
elapsedMs,
|
|
262
|
+
};
|
|
263
|
+
if (this.kind === 'image') {
|
|
264
|
+
const progress = { ...common, batchIndex: 0, batchSize: this.batchSize };
|
|
265
|
+
this.deps.state.updateJob(this.id, (r) => (r.job.progress = progress));
|
|
266
|
+
IMAGE_JOB_KIND.emitProgress(this.deps.emit, this.id, progress);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
const progress = common;
|
|
270
|
+
this.deps.state.updateJob(this.id, (r) => (r.job.progress = progress));
|
|
271
|
+
VIDEO_JOB_KIND.emitProgress(this.deps.emit, this.id, progress);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// ---------------------------------------------------------------------------
|
|
276
|
+
// Images
|
|
277
|
+
// ---------------------------------------------------------------------------
|
|
278
|
+
/** How long a provider usually takes for an image; the time-based fraction is drawn over it. */
|
|
279
|
+
const IMAGE_SCALE_MS = 20_000;
|
|
280
|
+
/** Validate, register and start an image job on a provider's model. */
|
|
281
|
+
export async function startRemoteImageJob(deps, request) {
|
|
282
|
+
const remote = remoteDeps(deps);
|
|
283
|
+
const target = request.target;
|
|
284
|
+
if (!target)
|
|
285
|
+
throw diffusionError('INVALID_REQUEST', 'A remote job needs a target.');
|
|
286
|
+
const resolved = await remote.gateway.image(target);
|
|
287
|
+
const workflow = checkRemoteImageRequest(request, resolved.descriptor);
|
|
288
|
+
checkCapacity(deps, remote);
|
|
289
|
+
const id = randomUUID().replaceAll('-', '');
|
|
290
|
+
const abort = new AbortController();
|
|
291
|
+
const job = {
|
|
292
|
+
id,
|
|
293
|
+
state: 'queued',
|
|
294
|
+
modelId: target.model,
|
|
295
|
+
request: withoutSources(request),
|
|
296
|
+
createdAtMs: deps.now(),
|
|
297
|
+
progress: null,
|
|
298
|
+
outputs: [],
|
|
299
|
+
};
|
|
300
|
+
const record = { kind: 'image', job, cancel: { requested: false }, remote: { target, abort } };
|
|
301
|
+
deps.state.insertJob(record);
|
|
302
|
+
IMAGE_JOB_KIND.emitJob(deps.emit, structuredClone(job));
|
|
303
|
+
const done = runRemoteImage(deps, id, request, resolved, workflow, abort.signal).then((saved) => settle(deps, id, { ok: true, outputs: saved.items, bytes: saved.bytes }), (raw) => settle(deps, id, { ok: false, raw }));
|
|
304
|
+
return { id, done };
|
|
305
|
+
}
|
|
306
|
+
async function runRemoteImage(deps, id, request, resolved, workflow, signal) {
|
|
307
|
+
const { descriptor, adapter, target } = resolved;
|
|
308
|
+
const images = [];
|
|
309
|
+
let mask;
|
|
310
|
+
if (workflow !== 'create' && request.initImage) {
|
|
311
|
+
images.push(await readInput(request.initImage, deps));
|
|
312
|
+
for (const extra of request.referenceImages ?? [])
|
|
313
|
+
images.push(await readInput(extra, deps));
|
|
314
|
+
if (workflow === 'inpaint' && request.maskImage)
|
|
315
|
+
mask = await readInput(request.maskImage, deps);
|
|
316
|
+
}
|
|
317
|
+
const seed = seedFor(deps, request.seed, descriptor.params.seed);
|
|
318
|
+
const job = imageGenerationJob(request, descriptor, workflow, mask ? { images, mask } : { images }, seed);
|
|
319
|
+
if (signal.aborted)
|
|
320
|
+
throw cancelledError();
|
|
321
|
+
setJobState(deps, id, 'generating');
|
|
322
|
+
const startedAt = deps.now();
|
|
323
|
+
const feed = new RemoteProgressFeed(deps, id, 'image', startedAt, IMAGE_SCALE_MS, job.n);
|
|
324
|
+
feed.start();
|
|
325
|
+
try {
|
|
326
|
+
const outputs = await adapter.generate(resolved.context(signal), job, (progress) => feed.update(progress.fraction, progress.phase));
|
|
327
|
+
if (signal.aborted)
|
|
328
|
+
throw cancelledError();
|
|
329
|
+
feed.saving();
|
|
330
|
+
const dir = deps.state.outputDir();
|
|
331
|
+
const createdAtMs = deps.now();
|
|
332
|
+
const items = [];
|
|
333
|
+
const bytes = [];
|
|
334
|
+
for (const [index, output] of outputs.entries()) {
|
|
335
|
+
const size = imageSize(output.bytes);
|
|
336
|
+
const recipe = {
|
|
337
|
+
jobId: id,
|
|
338
|
+
index,
|
|
339
|
+
prompt: request.prompt,
|
|
340
|
+
negativePrompt: job.negativePrompt ?? null,
|
|
341
|
+
width: size?.width ?? request.width,
|
|
342
|
+
height: size?.height ?? request.height,
|
|
343
|
+
steps: job.steps ?? 0,
|
|
344
|
+
cfgScale: job.cfgScale ?? 0,
|
|
345
|
+
guidance: job.guidance ?? null,
|
|
346
|
+
seed: seed === undefined ? -1 : seed + index,
|
|
347
|
+
batchSeed: seed ?? -1,
|
|
348
|
+
batchSize: job.n,
|
|
349
|
+
samplingMethod: job.sampler ?? null,
|
|
350
|
+
flowShift: null,
|
|
351
|
+
workflow,
|
|
352
|
+
strength: job.strength ?? null,
|
|
353
|
+
model: { modelId: target.model, family: target.provider, displayName: descriptor.name, filename: '' },
|
|
354
|
+
engine: { kind: 'remote', provider: target.provider, api: descriptor.api },
|
|
355
|
+
createdAtMs,
|
|
356
|
+
durationMs: Math.max(createdAtMs - startedAt, 0),
|
|
357
|
+
};
|
|
358
|
+
const options = recipeImageOptions(job);
|
|
359
|
+
if (options)
|
|
360
|
+
recipe.remote = options;
|
|
361
|
+
const saved = await deps.gallery.saveRemote(dir, recipe, output.bytes, imageMime(output.bytes) ?? output.mime);
|
|
362
|
+
deps.state.updateJob(id, (r) => r.job.outputs.push(saved.item));
|
|
363
|
+
items.push(saved.item);
|
|
364
|
+
bytes.push(saved.bytes);
|
|
365
|
+
}
|
|
366
|
+
return { items, bytes };
|
|
367
|
+
}
|
|
368
|
+
finally {
|
|
369
|
+
feed.stop();
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
/** What a remote image was asked besides the common recipe fields; `undefined` when nothing. */
|
|
373
|
+
function recipeImageOptions(job) {
|
|
374
|
+
const out = {};
|
|
375
|
+
if (job.size)
|
|
376
|
+
out.size = job.size;
|
|
377
|
+
if (job.aspectRatio)
|
|
378
|
+
out.aspectRatio = job.aspectRatio;
|
|
379
|
+
if (job.resolution)
|
|
380
|
+
out.resolution = job.resolution;
|
|
381
|
+
if (job.quality)
|
|
382
|
+
out.quality = job.quality;
|
|
383
|
+
if (job.background)
|
|
384
|
+
out.background = job.background;
|
|
385
|
+
if (job.outputFormat)
|
|
386
|
+
out.outputFormat = job.outputFormat;
|
|
387
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
388
|
+
}
|
|
389
|
+
// ---------------------------------------------------------------------------
|
|
390
|
+
// Video
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
/** How long a provider usually takes for a clip; the time-based fraction is drawn over it. */
|
|
393
|
+
const VIDEO_SCALE_MS = 120_000;
|
|
394
|
+
const DEFAULT_POLL = { first: 5_000, max: 20_000 };
|
|
395
|
+
/** Validate, register, submit and follow a clip on a provider's model. */
|
|
396
|
+
export async function startRemoteVideoJob(deps, request) {
|
|
397
|
+
const remote = remoteDeps(deps);
|
|
398
|
+
const target = request.target;
|
|
399
|
+
if (!target)
|
|
400
|
+
throw diffusionError('INVALID_REQUEST', 'A remote job needs a target.');
|
|
401
|
+
const resolved = await remote.gateway.video(target);
|
|
402
|
+
const workflow = checkRemoteVideoRequest(request, resolved.descriptor);
|
|
403
|
+
checkCapacity(deps, remote);
|
|
404
|
+
const id = randomUUID().replaceAll('-', '');
|
|
405
|
+
const abort = new AbortController();
|
|
406
|
+
const job = {
|
|
407
|
+
id,
|
|
408
|
+
state: 'queued',
|
|
409
|
+
modelId: target.model,
|
|
410
|
+
request: withoutVideoSources(request),
|
|
411
|
+
createdAtMs: deps.now(),
|
|
412
|
+
progress: null,
|
|
413
|
+
outputs: [],
|
|
414
|
+
};
|
|
415
|
+
const record = { kind: 'video', job, cancel: { requested: false }, remote: { target, abort } };
|
|
416
|
+
deps.state.insertJob(record);
|
|
417
|
+
VIDEO_JOB_KIND.emitJob(deps.emit, structuredClone(job));
|
|
418
|
+
const done = submitAndFollow(deps, id, request, resolved, workflow, abort.signal).then((saved) => settle(deps, id, { ok: true, outputs: saved.items, bytes: saved.bytes }), (raw) => settle(deps, id, { ok: false, raw }));
|
|
419
|
+
return { id, done };
|
|
420
|
+
}
|
|
421
|
+
async function submitAndFollow(deps, id, request, resolved, workflow, signal) {
|
|
422
|
+
const remote = remoteDeps(deps);
|
|
423
|
+
const frames = {};
|
|
424
|
+
if (workflow === 'image-to-video') {
|
|
425
|
+
if (request.initImage)
|
|
426
|
+
frames.first = await readInput(request.initImage, deps);
|
|
427
|
+
if (request.endImage)
|
|
428
|
+
frames.last = await readInput(request.endImage, deps);
|
|
429
|
+
}
|
|
430
|
+
const seed = seedFor(deps, request.seed, resolved.descriptor.params.seed);
|
|
431
|
+
const job = videoGenerationJob(request, resolved.descriptor, workflow, frames, seed);
|
|
432
|
+
if (signal.aborted)
|
|
433
|
+
throw cancelledError();
|
|
434
|
+
const ctx = resolved.context(signal);
|
|
435
|
+
const submission = await resolved.adapter.submit(ctx, job);
|
|
436
|
+
const startedAtMs = deps.now();
|
|
437
|
+
const stored = {
|
|
438
|
+
jobId: id,
|
|
439
|
+
target: resolved.target,
|
|
440
|
+
submission: submission,
|
|
441
|
+
request: withoutVideoSources(withOptions(request, job)),
|
|
442
|
+
...(seed === undefined ? {} : { seed }),
|
|
443
|
+
createdAtMs: deps.state.record(id)?.job.createdAtMs ?? startedAtMs,
|
|
444
|
+
startedAtMs,
|
|
445
|
+
};
|
|
446
|
+
await remote.store
|
|
447
|
+
.put(stored)
|
|
448
|
+
.catch((error) => deps.log('warn', `remote video ${id}: could not record the job: ${String(error)}`));
|
|
449
|
+
return follow(deps, stored, resolved, signal);
|
|
450
|
+
}
|
|
451
|
+
/** The request with what the adapter was actually asked, so the recipe records it. */
|
|
452
|
+
function withOptions(request, job) {
|
|
453
|
+
const remote = {};
|
|
454
|
+
if (job.durationSecs !== undefined)
|
|
455
|
+
remote.durationSecs = job.durationSecs;
|
|
456
|
+
if (job.resolution)
|
|
457
|
+
remote.resolution = job.resolution;
|
|
458
|
+
if (job.aspectRatio)
|
|
459
|
+
remote.aspectRatio = job.aspectRatio;
|
|
460
|
+
if (job.size)
|
|
461
|
+
remote.size = job.size;
|
|
462
|
+
if (job.generateAudio !== undefined)
|
|
463
|
+
remote.generateAudio = job.generateAudio;
|
|
464
|
+
return { ...request, remote };
|
|
465
|
+
}
|
|
466
|
+
/** Poll a submitted clip to its end, download it, save it; the job's store entry goes either way. */
|
|
467
|
+
async function follow(deps, stored, resolved, signal) {
|
|
468
|
+
const remote = remoteDeps(deps);
|
|
469
|
+
const { adapter } = resolved;
|
|
470
|
+
const id = stored.jobId;
|
|
471
|
+
const ctx = resolved.context(signal);
|
|
472
|
+
const cancel = adapter.cancel?.bind(adapter);
|
|
473
|
+
if (cancel)
|
|
474
|
+
deps.state.updateJob(id, (record) => {
|
|
475
|
+
if (record.remote)
|
|
476
|
+
record.remote.cancelUpstream = () => cancel(resolved.context(AbortSignal.timeout(15_000)), stored.submission);
|
|
477
|
+
});
|
|
478
|
+
setJobState(deps, id, 'generating');
|
|
479
|
+
const feed = new RemoteProgressFeed(deps, id, 'video', stored.startedAtMs, VIDEO_SCALE_MS);
|
|
480
|
+
feed.start();
|
|
481
|
+
try {
|
|
482
|
+
const interval = adapter.pollIntervalMs ?? DEFAULT_POLL;
|
|
483
|
+
let wait = interval.first;
|
|
484
|
+
let failures = 0;
|
|
485
|
+
let result;
|
|
486
|
+
while (!result) {
|
|
487
|
+
if (signal.aborted)
|
|
488
|
+
throw cancelledError();
|
|
489
|
+
if (deps.now() - stored.startedAtMs > REMOTE_VIDEO_CEILING_MS)
|
|
490
|
+
throw diffusionError('REMOTE_TIMEOUT', `${resolved.target.provider} did not finish the clip in time.`);
|
|
491
|
+
let poll;
|
|
492
|
+
try {
|
|
493
|
+
poll = await adapter.poll(ctx, stored.submission);
|
|
494
|
+
failures = 0;
|
|
495
|
+
}
|
|
496
|
+
catch (error) {
|
|
497
|
+
if (signal.aborted)
|
|
498
|
+
throw cancelledError();
|
|
499
|
+
const code = error.code;
|
|
500
|
+
const transient = code === 'REMOTE_UNAVAILABLE' || code === 'REMOTE_TIMEOUT' || code === 'REMOTE_RATE_LIMITED';
|
|
501
|
+
if (!transient || ++failures > MAX_POLL_FAILURES)
|
|
502
|
+
throw error;
|
|
503
|
+
deps.log('debug', `remote video ${id}: poll failed (${failures}): ${String(error.message)}`);
|
|
504
|
+
}
|
|
505
|
+
if (poll?.state === 'completed') {
|
|
506
|
+
result = poll.result;
|
|
507
|
+
break;
|
|
508
|
+
}
|
|
509
|
+
if (poll?.state === 'failed')
|
|
510
|
+
throw poll.error;
|
|
511
|
+
if (poll)
|
|
512
|
+
feed.update(poll.progress, poll.state === 'queued' ? 'queued' : 'generating');
|
|
513
|
+
await ctx.sleep(wait);
|
|
514
|
+
wait = Math.min(Math.round(wait * 1.5), interval.max);
|
|
515
|
+
}
|
|
516
|
+
const file = await adapter.download(ctx, stored.submission, result);
|
|
517
|
+
if (signal.aborted)
|
|
518
|
+
throw cancelledError();
|
|
519
|
+
feed.saving();
|
|
520
|
+
return await saveClip(deps, stored, resolved.descriptor, file.bytes);
|
|
521
|
+
}
|
|
522
|
+
finally {
|
|
523
|
+
feed.stop();
|
|
524
|
+
// A core shutting down keeps the entry: the next one polls the clip again.
|
|
525
|
+
if (!deps.state.closing)
|
|
526
|
+
await remote.store.remove(id).catch(() => { });
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
async function saveClip(deps, stored, model, bytes) {
|
|
530
|
+
const facts = mp4Facts(bytes);
|
|
531
|
+
const isMp4 = bytes.length >= 12 && bytes.toString('latin1', 4, 8) === 'ftyp';
|
|
532
|
+
const request = stored.request;
|
|
533
|
+
const options = request.remote ?? {};
|
|
534
|
+
const createdAtMs = deps.now();
|
|
535
|
+
const recipe = {
|
|
536
|
+
jobId: stored.jobId,
|
|
537
|
+
prompt: request.prompt,
|
|
538
|
+
negativePrompt: request.negativePrompt ? request.negativePrompt : null,
|
|
539
|
+
width: facts?.width || request.width,
|
|
540
|
+
height: facts?.height || request.height,
|
|
541
|
+
frames: 0,
|
|
542
|
+
frameCount: 0,
|
|
543
|
+
fps: 0,
|
|
544
|
+
steps: request.steps,
|
|
545
|
+
cfgScale: request.cfgScale,
|
|
546
|
+
guidance: null,
|
|
547
|
+
seed: stored.seed ?? -1,
|
|
548
|
+
samplingMethod: null,
|
|
549
|
+
flowShift: null,
|
|
550
|
+
workflow: request.workflow ?? 'create',
|
|
551
|
+
outputFormat: isMp4 ? 'mp4' : 'webm',
|
|
552
|
+
model: {
|
|
553
|
+
modelId: stored.target.model,
|
|
554
|
+
family: stored.target.provider,
|
|
555
|
+
displayName: model.name,
|
|
556
|
+
filename: '',
|
|
557
|
+
},
|
|
558
|
+
engine: { kind: 'remote', provider: stored.target.provider, api: model.api },
|
|
559
|
+
createdAtMs,
|
|
560
|
+
durationMs: Math.max(createdAtMs - stored.startedAtMs, 0),
|
|
561
|
+
};
|
|
562
|
+
const seconds = facts?.durationSecs || options.durationSecs;
|
|
563
|
+
if (seconds)
|
|
564
|
+
recipe.durationSecs = seconds;
|
|
565
|
+
if (Object.keys(options).length > 0)
|
|
566
|
+
recipe.remote = options;
|
|
567
|
+
const saved = await deps.videoGallery.save(deps.state.videoOutputDir(), recipe, bytes);
|
|
568
|
+
deps.videoSaved?.();
|
|
569
|
+
deps.state.updateJob(stored.jobId, (record) => record.job.outputs.push(saved.item));
|
|
570
|
+
return { items: [saved.item], bytes: [saved.bytes] };
|
|
571
|
+
}
|
|
572
|
+
/**
|
|
573
|
+
* Follow again every clip the last core submitted and did not finish. A job whose provider is gone
|
|
574
|
+
* fails at once; none is ever submitted twice.
|
|
575
|
+
*/
|
|
576
|
+
export async function resumeRemoteVideoJobs(deps) {
|
|
577
|
+
const remote = deps.remote;
|
|
578
|
+
if (!remote)
|
|
579
|
+
return;
|
|
580
|
+
const stored = await remote.store.list().catch(() => []);
|
|
581
|
+
for (const entry of stored) {
|
|
582
|
+
if (deps.state.record(entry.jobId))
|
|
583
|
+
continue;
|
|
584
|
+
const abort = new AbortController();
|
|
585
|
+
const job = {
|
|
586
|
+
id: entry.jobId,
|
|
587
|
+
state: 'generating',
|
|
588
|
+
modelId: entry.target.model,
|
|
589
|
+
request: entry.request,
|
|
590
|
+
createdAtMs: entry.createdAtMs,
|
|
591
|
+
startedAtMs: entry.startedAtMs,
|
|
592
|
+
progress: null,
|
|
593
|
+
outputs: [],
|
|
594
|
+
};
|
|
595
|
+
deps.state.insertJob({
|
|
596
|
+
kind: 'video',
|
|
597
|
+
job,
|
|
598
|
+
cancel: { requested: false },
|
|
599
|
+
remote: { target: entry.target, abort },
|
|
600
|
+
});
|
|
601
|
+
VIDEO_JOB_KIND.emitJob(deps.emit, structuredClone(job));
|
|
602
|
+
void (async () => {
|
|
603
|
+
try {
|
|
604
|
+
const resolved = await remote.gateway.video(entry.target);
|
|
605
|
+
await waitConfigured(deps, abort.signal);
|
|
606
|
+
const saved = await follow(deps, entry, resolved, abort.signal);
|
|
607
|
+
settle(deps, entry.jobId, { ok: true, outputs: saved.items, bytes: saved.bytes });
|
|
608
|
+
}
|
|
609
|
+
catch (raw) {
|
|
610
|
+
if (deps.state.closing)
|
|
611
|
+
return;
|
|
612
|
+
await remote.store.remove(entry.jobId).catch(() => { });
|
|
613
|
+
settle(deps, entry.jobId, { ok: false, raw });
|
|
614
|
+
}
|
|
615
|
+
})();
|
|
616
|
+
deps.log('info', `remote video ${entry.jobId}: following it again at ${entry.target.provider}`);
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
/** A resumed clip saves into the folder the app configures; without an app, the default one. */
|
|
620
|
+
async function waitConfigured(deps, signal) {
|
|
621
|
+
const deadline = deps.now() + RESUME_CONFIG_WAIT_MS;
|
|
622
|
+
while (!deps.state.configured && deps.now() < deadline && !signal.aborted)
|
|
623
|
+
await deps.sleep(500);
|
|
624
|
+
}
|
|
625
|
+
// ---------------------------------------------------------------------------
|
|
626
|
+
// Cancel
|
|
627
|
+
// ---------------------------------------------------------------------------
|
|
628
|
+
/** Stop a remote job: every request and wait aborts, and the provider is asked to stop it too. */
|
|
629
|
+
export async function cancelRemoteJob(deps, id) {
|
|
630
|
+
const record = deps.state.record(id);
|
|
631
|
+
if (!record?.remote)
|
|
632
|
+
throw diffusionError('JOB_NOT_FOUND', 'That job no longer exists.');
|
|
633
|
+
if (isTerminalJobState(record.job.state))
|
|
634
|
+
return { cancelled: record.job.state === 'cancelled', serverStopped: false };
|
|
635
|
+
record.cancel.requested = true;
|
|
636
|
+
record.remote.abort.abort();
|
|
637
|
+
finishJob(deps, id, { ok: false, error: errorBody(cancelledError()) });
|
|
638
|
+
const upstream = record.remote.cancelUpstream;
|
|
639
|
+
if (upstream)
|
|
640
|
+
await upstream().catch((error) => deps.log('debug', `remote cancel of ${id}: ${String(error)}`));
|
|
641
|
+
return { cancelled: true, serverStopped: false };
|
|
642
|
+
}
|
|
643
|
+
//# sourceMappingURL=remote-job.js.map
|